Add encryption methods and encrypt device when sent to server

This commit is contained in:
William Oldham 2023-11-17 11:58:28 +00:00
parent 0dc3e51a36
commit 7f474af657
4 changed files with 107 additions and 22 deletions

View File

@ -4,6 +4,12 @@ import { generateMnemonic, validateMnemonic } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english"; import { wordlist } from "@scure/bip39/wordlists/english";
import forge from "node-forge"; import forge from "node-forge";
type Keys = {
privateKey: Uint8Array;
publicKey: Uint8Array;
seed: Uint8Array;
};
async function seedFromMnemonic(mnemonic: string) { async function seedFromMnemonic(mnemonic: string) {
return pbkdf2Async(sha256, mnemonic, "mnemonic", { return pbkdf2Async(sha256, mnemonic, "mnemonic", {
c: 2048, c: 2048,
@ -15,7 +21,7 @@ export function verifyValidMnemonic(mnemonic: string) {
return validateMnemonic(mnemonic, wordlist); return validateMnemonic(mnemonic, wordlist);
} }
export async function keysFromMnemonic(mnemonic: string) { export async function keysFromMnemonic(mnemonic: string): Promise<Keys> {
const seed = await seedFromMnemonic(mnemonic); const seed = await seedFromMnemonic(mnemonic);
const { privateKey, publicKey } = forge.pki.ed25519.generateKeyPair({ const { privateKey, publicKey } = forge.pki.ed25519.generateKeyPair({
@ -25,6 +31,7 @@ export async function keysFromMnemonic(mnemonic: string) {
return { return {
privateKey, privateKey,
publicKey, publicKey,
seed,
}; };
} }
@ -34,8 +41,8 @@ export function genMnemonic(): string {
export async function signCode( export async function signCode(
code: string, code: string,
privateKey: forge.pki.ed25519.NativeBuffer privateKey: Uint8Array
): Promise<forge.pki.ed25519.NativeBuffer> { ): Promise<Uint8Array> {
return forge.pki.ed25519.sign({ return forge.pki.ed25519.sign({
encoding: "utf8", encoding: "utf8",
message: code, message: code,
@ -43,18 +50,85 @@ export async function signCode(
}); });
} }
export function bytesToBase64(bytes: Uint8Array) {
return forge.util.encode64(String.fromCodePoint(...bytes));
}
export function bytesToBase64Url(bytes: Uint8Array): string { export function bytesToBase64Url(bytes: Uint8Array): string {
return btoa(String.fromCodePoint(...bytes)) return bytesToBase64(bytes)
.replace(/\//g, "_") .replace(/\//g, "_")
.replace(/\+/g, "-") .replace(/\+/g, "-")
.replace(/=+$/, ""); .replace(/=+$/, "");
} }
export async function signChallenge(mnemonic: string, challengeCode: string) { export async function signChallenge(keys: Keys, challengeCode: string) {
const keys = await keysFromMnemonic(mnemonic);
const signature = await signCode(challengeCode, keys.privateKey); const signature = await signCode(challengeCode, keys.privateKey);
return { return bytesToBase64Url(signature);
publicKey: bytesToBase64Url(keys.publicKey), }
signature: bytesToBase64Url(signature),
}; export function base64ToBuffer(data: string) {
return forge.util.binary.base64.decode(data);
}
export function base64ToStringBugger(data: string) {
return forge.util.createBuffer(base64ToBuffer(data));
}
export function stringBufferToBase64(buffer: forge.util.ByteStringBuffer) {
return forge.util.encode64(buffer.getBytes());
}
export async function encryptData(data: string, secret: Uint8Array) {
if (secret.byteLength !== 32)
throw new Error("Secret must be at least 256-bit");
const iv = await new Promise<string>((resolve, reject) => {
forge.random.getBytes(16, (err, bytes) => {
if (err) reject(err);
resolve(bytes);
});
});
const cipher = forge.cipher.createCipher(
"AES-GCM",
forge.util.createBuffer(secret)
);
cipher.start({
iv,
tagLength: 128,
});
cipher.update(forge.util.createBuffer(data));
cipher.finish();
const encryptedData = cipher.output;
const tag = cipher.mode.tag;
return `${forge.util.encode64(iv)}.${stringBufferToBase64(
encryptedData
)}.${stringBufferToBase64(tag)}` as const;
}
export async function decryptData(
data: `${string}.${string}.${string}`,
secret: Uint8Array
) {
if (secret.byteLength !== 32) throw new Error("Secret must be 256-bit");
const [iv, encryptedData, tag] = data.split(".");
const decipher = forge.cipher.createDecipher(
"AES-GCM",
forge.util.createBuffer(secret)
);
decipher.start({
iv: base64ToStringBugger(iv),
tag: base64ToStringBugger(tag),
tagLength: 128,
});
decipher.update(base64ToStringBugger(encryptedData));
const pass = decipher.finish();
if (!pass) throw new Error("Error decrypting data");
return decipher.output.toString();
} }

View File

@ -2,7 +2,9 @@ import { useCallback } from "react";
import { removeSession } from "@/backend/accounts/auth"; import { removeSession } from "@/backend/accounts/auth";
import { import {
bytesToBase64,
bytesToBase64Url, bytesToBase64Url,
encryptData,
keysFromMnemonic, keysFromMnemonic,
signChallenge, signChallenge,
} from "@/backend/accounts/crypto"; } from "@/backend/accounts/crypto";
@ -49,22 +51,24 @@ export function useAuth() {
const login = useCallback( const login = useCallback(
async (loginData: LoginData) => { async (loginData: LoginData) => {
const keys = await keysFromMnemonic(loginData.mnemonic); const keys = await keysFromMnemonic(loginData.mnemonic);
const publicKeyBase64Url = bytesToBase64Url(keys.publicKey);
const { challenge } = await getLoginChallengeToken( const { challenge } = await getLoginChallengeToken(
backendUrl, backendUrl,
bytesToBase64Url(keys.publicKey) publicKeyBase64Url
); );
const signResult = await signChallenge(loginData.mnemonic, challenge); const signature = await signChallenge(keys, challenge);
const loginResult = await loginAccount(backendUrl, { const loginResult = await loginAccount(backendUrl, {
challenge: { challenge: {
code: challenge, code: challenge,
signature: signResult.signature, signature,
}, },
publicKey: signResult.publicKey, publicKey: publicKeyBase64Url,
device: loginData.userData.device, device: await encryptData(loginData.userData.device, keys.seed),
}); });
const user = await getUser(backendUrl, loginResult.token); const user = await getUser(backendUrl, loginResult.token);
await userDataLogin(loginResult, user); const seedBase64 = bytesToBase64(keys.seed);
await userDataLogin(loginResult, user, seedBase64);
}, },
[userDataLogin, backendUrl] [userDataLogin, backendUrl]
); );
@ -86,18 +90,23 @@ export function useAuth() {
const register = useCallback( const register = useCallback(
async (registerData: RegistrationData) => { async (registerData: RegistrationData) => {
const { challenge } = await getRegisterChallengeToken(backendUrl); const { challenge } = await getRegisterChallengeToken(backendUrl);
const signResult = await signChallenge(registerData.mnemonic, challenge); const keys = await keysFromMnemonic(registerData.mnemonic);
const signature = await signChallenge(keys, challenge);
const registerResult = await registerAccount(backendUrl, { const registerResult = await registerAccount(backendUrl, {
challenge: { challenge: {
code: challenge, code: challenge,
signature: signResult.signature, signature,
}, },
publicKey: signResult.publicKey, publicKey: bytesToBase64Url(keys.publicKey),
device: registerData.userData.device, device: await encryptData(registerData.userData.device, keys.seed),
profile: registerData.userData.profile, profile: registerData.userData.profile,
}); });
await userDataLogin(registerResult, registerResult.user); await userDataLogin(
registerResult,
registerResult.user,
bytesToBase64(keys.seed)
);
}, },
[backendUrl, userDataLogin] [backendUrl, userDataLogin]
); );

View File

@ -23,12 +23,13 @@ export function useAuthData() {
const replaceItems = useProgressStore((s) => s.replaceItems); const replaceItems = useProgressStore((s) => s.replaceItems);
const login = useCallback( const login = useCallback(
async (account: LoginResponse, user: UserResponse) => { async (account: LoginResponse, user: UserResponse, seed: string) => {
setAccount({ setAccount({
token: account.token, token: account.token,
userId: user.id, userId: user.id,
sessionId: account.session.id, sessionId: account.session.id,
profile: user.profile, profile: user.profile,
seed,
}); });
}, },
[setAccount] [setAccount]

View File

@ -14,6 +14,7 @@ export type AccountWithToken = Account & {
sessionId: string; sessionId: string;
userId: string; userId: string;
token: string; token: string;
seed: string;
}; };
interface AuthStore { interface AuthStore {