diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 7edd57b..a49496a 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -48,7 +48,7 @@ function base64ToUint8(base64: string) { export async function deriveKeyVault(secret: string) { console.log(crypto); - + let hash = strToUint8(secret), last = hash; for (let i = 0; i < 10000; i++) { const hashInput = Uint8Array.from([...strToUint8(SALT), ...hash]); @@ -57,14 +57,14 @@ export async function deriveKeyVault(secret: string) { hash = new Uint8Array(hashBuffer); } - const symmetricKey = last.slice(0, 32); - const privateKey = hash.slice(0, 32); - const publicKey = ed.getPublicKey(privateKey); + const symmKey = last.slice(0, 32); + const privKey = hash.slice(0, 32); + const pubKey = ed.getPublicKey(privKey); return { - publicKey, - privateKey, - symmetricKey, + pubKey, + privKey, + symmKey, }; } @@ -85,10 +85,10 @@ export function decodeKeyVault(encoded: string) { } -export async function encrypt(vault: KeyVault, data: Uint8Array) { +export async function encrypt(symmKey: Uint8Array, data: Uint8Array) { const key = await crypto.subtle.importKey( 'raw', - vault.symmetricKey, + symmKey, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'], @@ -105,10 +105,10 @@ export async function encrypt(vault: KeyVault, data: Uint8Array) { return Uint8Array.from([...iv, ...new Uint8Array(encryptedBuffer)]); } -export async function decrypt(vault: KeyVault, cipher: Uint8Array) { +export async function decrypt(symmKey: Uint8Array, cipher: Uint8Array) { const key = await crypto.subtle.importKey( 'raw', - vault.symmetricKey, + symmKey, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'], @@ -125,3 +125,15 @@ export async function decrypt(vault: KeyVault, cipher: Uint8Array) { return new Uint8Array(dataBuffer); } + +export async function sign(privKey: Uint8Array, data: Uint8Array): Promise { + return await ed.signAsync(data, privKey) as Uint8Array; +} + +export async function verify(pubKey: Uint8Array, signature: Uint8Array, data: Uint8Array): Promise { + try { + return await ed.verifyAsync(signature, data, pubKey); + } catch (_) { + return false; + } +} diff --git a/src/routes/(area)/log/+page.svelte b/src/routes/(area)/log/+page.svelte index 836d7fc..467016e 100644 --- a/src/routes/(area)/log/+page.svelte +++ b/src/routes/(area)/log/+page.svelte @@ -15,7 +15,7 @@ } const vault = getKeyVault(); - fetch(`/api/log?pubkey=${encodeURIComponent(uint8ToHex(vault!.publicKey))}`).then(async res => { + fetch(`/api/log?pubkey=${encodeURIComponent(uint8ToHex(vault!.pubKey))}`).then(async res => { if (!res.ok) return; entryUrls = (await res.json() as any[]).sort((a, b) => b.date.localeCompare(a.date)); @@ -35,7 +35,7 @@ console.log(cipherBuffer); const vault = getKeyVault()!; - const videoData = await decrypt(vault, new Uint8Array(cipherBuffer)); + const videoData = await decrypt(vault.symmKey, new Uint8Array(cipherBuffer)); const blob = new Blob([videoData], { type: 'video/webm' }); videoSrc = URL.createObjectURL(blob); diff --git a/src/routes/(area)/today/+page.svelte b/src/routes/(area)/today/+page.svelte index 1f32ae4..626d7f6 100644 --- a/src/routes/(area)/today/+page.svelte +++ b/src/routes/(area)/today/+page.svelte @@ -3,7 +3,7 @@ import { hashBuffer } from "$lib/hash"; import { dateDiff } from "$lib/date"; import { getKeyVault, loggedIn } from "$lib/auth"; - import { encrypt, uint8ToHex } from "$lib/crypto"; + import { encrypt, sign, uint8ToHex } from "$lib/crypto"; import { lang } from "$lib/lang"; type StatusT = "pending" | "active" | "error"; @@ -59,7 +59,10 @@ recorder.addEventListener('dataavailable', async (e) => { const inputBlob = new Blob([e.data], { type: 'video/mp4' }); - const convertRes = await fetch('/api/convert', { + const vault = getKeyVault()!; + const signature = await sign(vault.privKey, new Uint8Array(await inputBlob.arrayBuffer())); + + const convertRes = await fetch(`/api/convert?pubkey=${encodeURIComponent(uint8ToHex(vault.pubKey))}&signature=${encodeURIComponent(uint8ToHex(signature))}`, { method: 'POST', body: inputBlob, }); @@ -87,13 +90,12 @@ const blob = await (await fetch(recordingURL)).blob(); const buffer = await blob.arrayBuffer(); - console.log('buffer', buffer); const vault = getKeyVault()!; - const encrypted = await encrypt(vault, new Uint8Array(buffer)); - console.log('encrypted', encrypted); + const encrypted = await encrypt(vault.symmKey, new Uint8Array(buffer)); + const signature = await sign(vault.privKey, encrypted); - const res = await fetch(`/api/today?pubkey=${uint8ToHex(vault.publicKey)}`, { + const res = await fetch(`/api/today?pubkey=${uint8ToHex(vault.pubKey)}&signature=${uint8ToHex(signature)}`, { method: 'POST', body: encrypted, headers: { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 7d6f443..4b6768c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,9 +6,9 @@ if (typeof window !== 'object') return; document.body.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); - window.addEventListener('resize', () => { - document.body.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); - }); + // window.addEventListener('resize', () => { + // document.body.style.setProperty('--vh', `${window.innerHeight * 0.01}px`); + // }); }); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 6bb6759..fb6f608 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -5,8 +5,6 @@ onMount(() => { if (typeof window !== 'object') return; - console.log('test', loggedIn()); - if (loggedIn()) { window.location.href = '/today'; } diff --git a/src/routes/api/convert/+server.ts b/src/routes/api/convert/+server.ts index f4a0c16..0818f2e 100644 --- a/src/routes/api/convert/+server.ts +++ b/src/routes/api/convert/+server.ts @@ -1,10 +1,22 @@ import { convertVideo } from "$lib/ffmpeg.server"; import { error } from "@sveltejs/kit"; import type { RequestHandler } from "./$types"; +import { hexToUint8, verify } from "$lib/crypto"; + +export const POST: RequestHandler = async ({ request, url }) => { + const pubkeyHex = url.searchParams.get('pubkey')?.trim()?.toLowerCase(); + if (!pubkeyHex || !/^[0-9a-f]{64}$/.test(pubkeyHex)) error(400); + const pubKey = hexToUint8(pubkeyHex); + + const signatureHex = url.searchParams.get('signature')?.trim()?.toLowerCase(); + if (!signatureHex || !/^[0-9a-f]+$/.test(signatureHex)) error(400); + const signature = hexToUint8(signatureHex); -export const POST: RequestHandler = async ({ request }) => { const bodyBuffer = await request.arrayBuffer(); const bodyUint8 = new Uint8Array(bodyBuffer); + + const valid = await verify(pubKey, signature, bodyUint8); + if (!valid) error(401); const convertedUint8 = await convertVideo(bodyUint8); if (!convertedUint8) error(500); diff --git a/src/routes/api/today/+server.ts b/src/routes/api/today/+server.ts index 63c829d..0a7fe99 100644 --- a/src/routes/api/today/+server.ts +++ b/src/routes/api/today/+server.ts @@ -4,17 +4,26 @@ import type { RequestHandler } from "./$types"; import { PutObjectCommand } from "@aws-sdk/client-s3"; import { env } from "$env/dynamic/private"; import { s3 } from "$lib/storage.server"; +import { hexToUint8, verify } from "$lib/crypto"; +// This route is currently vulnerable against replay attacks across multiple days +// -- possible fix: prepend date to signature verification payload export const POST: RequestHandler = async ({ request, url }) => { const pubkeyHex = url.searchParams.get('pubkey')?.trim()?.toLowerCase(); if (!pubkeyHex || !/^[0-9a-f]{64}$/.test(pubkeyHex)) error(400); + const pubKey = hexToUint8(pubkeyHex); + + const signatureHex = url.searchParams.get('signature')?.trim()?.toLowerCase(); + if (!signatureHex || !/^[0-9a-f]+$/.test(signatureHex)) error(400); + const signature = hexToUint8(signatureHex); const today = new Date().toISOString().slice(0, 10); const bodyBuffer = await request.arrayBuffer(); const bodyUint8 = new Uint8Array(bodyBuffer); - console.log(today, bodyUint8.byteLength); + const valid = await verify(pubKey, signature, bodyUint8); + if (!valid) error(401); const command = new PutObjectCommand({ Bucket: env.S3_BUCKET,