Skip to content

Commit

Permalink
feat: encrypt QR and remove token from QR code (#195)
Browse files Browse the repository at this point in the history
* feat: encrypt QR and remove token from QR code

* test: update integration tests

* chore: rename qr-data
  • Loading branch information
edalholt authored Sep 18, 2023
1 parent c44c44d commit 7b472c2
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 58 deletions.
38 changes: 13 additions & 25 deletions backend/controllers/qr.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Response } from "express";
import { getNtnuiProfile, refreshNtnuiToken } from "ntnui-tools";
import { Assembly } from "../models/assembly";
import { Log } from "../models/log";
import { User } from "../models/user";
Expand All @@ -9,24 +8,18 @@ import {
notifyOneParticipant,
notifyOrganizers,
} from "../utils/socketNotifier";
import { decrypt, encrypt } from "../utils/crypto";

export async function getToken(req: RequestWithNtnuiNo, res: Response) {
export async function getQRData(req: RequestWithNtnuiNo, res: Response) {
if (!req.ntnuiNo) {
return res.status(401).json({ message: "Unauthorized" });
}
try {
const { refreshToken } = req.cookies;
// Refresh token to ensure token is fresh (maximum lifespan).
const accessToken = await refreshNtnuiToken(refreshToken);
return res
.cookie("accessToken", accessToken.access, {
maxAge: 1800000, // 30 minutes
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: true,
})
.status(200)
.json(accessToken);
return res.status(200).json({
QRData: encrypt(
JSON.stringify({ ntnuiNo: req.ntnuiNo, timestamp: Date.now() })
),
});
} catch (error) {
return res.status(401).json({ message: "Unauthorized" });
}
Expand All @@ -37,24 +30,19 @@ export async function assemblyCheckin(req: RequestWithNtnuiNo, res: Response) {
return res.status(401).json({ message: "Unauthorized" });
}
const group = req.body.group;
const token = req.body.token;
const timestamp = req.body.timestamp;
const user = await User.findById(req.ntnuiNo);
const { ntnuiNo, timestamp } = JSON.parse(decrypt(req.body.QRData));
const organizerUser = await User.findById(req.ntnuiNo);

if (
user &&
user.groups.some(
organizerUser &&
organizerUser.groups.some(
(membership) => membership.organizer && membership.groupSlug == group
)
) {
try {
const scannedUser = await User.findById(
(
await getNtnuiProfile(token)
).data.ntnui_no
);
const scannedUser = await User.findById(ntnuiNo);

// If user is logged inn, the correct token is provided,
// If user is logged in, the correct userID is provided,
// and timestamp is less than 15 seconds ago and not negative (created before current time)
if (
scannedUser &&
Expand Down
4 changes: 2 additions & 2 deletions backend/routes/qr.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Router } from "express";
import { assemblyCheckin, getToken } from "../controllers/qr";
import { assemblyCheckin, getQRData } from "../controllers/qr";
import authorization from "../utils/authorizationMiddleware";

const qrRoutes = Router();

qrRoutes.get("/", authorization, getToken);
qrRoutes.get("/", authorization, getQRData);

qrRoutes.post("/checkin", authorization, assemblyCheckin);

Expand Down
8 changes: 3 additions & 5 deletions backend/tests/votation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import request from "supertest";
import app from "../index";
import {
accessToken,
activateAssemblyTest,
createAssemblyTest,
deactivateAssemblyTest,
Expand All @@ -15,6 +14,7 @@ import {
} from "./utils";
import { cookies } from "./utils";
import mongoose from "mongoose";
import { encrypt } from "../utils/crypto";

afterAll(async () => {
await mongoose.connection.close();
Expand Down Expand Up @@ -71,8 +71,7 @@ describe("API test: test CRUD operations on a vote, also testing check-in of use
.set("Cookie", cookies)
.send({
group: "sprint",
timestamp: Date.now(),
token: accessToken,
QRData: encrypt(JSON.stringify({ ntnuiNo: 1, timestamp: Date.now() })),
})
.then((response) => {
expect(response.statusCode).toBe(200);
Expand Down Expand Up @@ -103,8 +102,7 @@ describe("API test: test CRUD operations on a vote, also testing check-in of use
.set("Cookie", cookies)
.send({
group: "sprint",
timestamp: Date.now(),
token: accessToken,
QRData: encrypt(JSON.stringify({ ntnuiNo: 1, timestamp: Date.now() })),
})
.then((response) => {
expect(response.statusCode).toBe(400);
Expand Down
19 changes: 19 additions & 0 deletions backend/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as crypto from "crypto";

const algorithm = "aes-256-cbc";
const key = process.env.CRYPTO_KEY || crypto.randomBytes(32);
const iv = process.env.CRYPTO_IV || crypto.randomBytes(16);

export function encrypt(text: string) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
return encrypted;
}

export function decrypt(text: string) {
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(text, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
9 changes: 9 additions & 0 deletions frontend/src/assets/ntnuiColor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 9 additions & 19 deletions frontend/src/components/QrCode.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,33 @@
import { Loader } from "@mantine/core";
import { useContext, useEffect, useState } from "react";
import { QRCodeSVG } from "qrcode.react";
import { getQrInfo } from "../services/qr";
import { getQrData } from "../services/qr";
import logo from "../assets/ntnuiColor.svg";
import { useNavigate } from "react-router-dom";
import { checkedInState, checkedInType } from "../utils/Context";

export function QrCode() {
const { groupSlug } = useContext(checkedInState) as checkedInType;
const navigate = useNavigate();
let [access, setAccess] = useState<string>();
let [time, setTime] = useState<number>(Date.now());
const getCredentials = async () => {
setAccess((await getQrInfo()).access);
const [QRData, setQRData] = useState<string>();
const updateQR = async () => {
setQRData((await getQrData()).QRData);
};

// Update timestamp every 10 seconds
// Update QR every 10 seconds
useEffect(() => {
if (!groupSlug) {
navigate("/start");
}

const interval = setInterval(() => setTime(Date.now()), 10000);
updateQR();
const interval = setInterval(() => updateQR(), 10000);
return () => {
clearInterval(interval);
};
}, []);

// Set token on mount and update every 5 minutes.
useEffect(() => {
getCredentials();
const interval = setInterval(() => getCredentials(), 300000);
return () => {
clearInterval(interval);
};
}, []);

return !access ? (
return !QRData ? (
<Loader></Loader>
) : (
<QRCodeSVG
Expand All @@ -46,8 +37,7 @@ export function QrCode() {
includeMargin={true}
size={350}
value={JSON.stringify({
access: access,
timestamp: time,
QRData: QRData,
group: groupSlug,
})}
/>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/CheckIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export function CheckIn() {
if (result) {
try {
const decodedResult = JSON.parse(result);
const { access, group, timestamp } = decodedResult;
checkInUser({ access, group, timestamp });
const { QRData, group } = decodedResult;
checkInUser({ QRData, group });
} catch (error) {
showNotification({
title: "Error",
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/services/qr.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from "axios";
import { QRType } from "../types/checkin";

export const getQrInfo = async (): Promise<{ access: string }> => {
export const getQrData = async (): Promise<{ QRData: string }> => {
return (await axios.get("/qr")).data;
};

Expand All @@ -10,9 +10,8 @@ export const assemblyCheckin = async (
): Promise<{ title: string; message: string }> => {
try {
const res = await axios.post("/qr/checkin", {
QRData: qrScan.QRData,
group: qrScan.group,
token: qrScan.access,
timestamp: qrScan.timestamp,
});
if (res.status == 200) {
return {
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/types/checkin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export type QRType = {
access: string;
QRData: string;
group: string;
timestamp: number;
};

0 comments on commit 7b472c2

Please sign in to comment.