From 435976e837edf755236a7b49eabf67166270383c Mon Sep 17 00:00:00 2001 From: Pascal Perrenoud Date: Tue, 14 May 2024 15:24:01 +0200 Subject: [PATCH] Implement sym box --- src/boxes/symmetric.ts | 64 ++++++++++++++++++++++++++++++------ test/boxes/symmetric.test.ts | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 test/boxes/symmetric.test.ts diff --git a/src/boxes/symmetric.ts b/src/boxes/symmetric.ts index d2d0081..9fa5ef0 100644 --- a/src/boxes/symmetric.ts +++ b/src/boxes/symmetric.ts @@ -1,23 +1,67 @@ -import type {Result} from 'result' +import {Result} from 'result' +import * as misc from 'misc' -export type Key = void +export type Key = CryptoKey export class SecretBox { - public static gen_key() : Key { - throw "todo" + private readonly cipher: Uint8Array + private readonly iv: Uint8Array + private readonly phantom!: T + + private constructor(iv: Uint8Array, cipher: Uint8Array) { + this.iv = iv + this.cipher = cipher } - public static encrypt(key: Key, data: Uint8Array) : SecretBox { - throw "todo" + public static async gen_key(extractable: boolean = false): Promise { + return window.crypto.subtle.generateKey( + { + name: "AES-GCM", + length: 256, + }, + extractable, + ["encrypt", "decrypt"] + ) } - public decrypt(key: Key) : Result { - throw "todo" + + public static async encrypt(key: Key, data: Uint8Array): Promise>> { + const iv = window.crypto.getRandomValues(new Uint8Array(12)) + const algorithm = {name: "AES-GCM", iv} + + try { + const cipher = await window.crypto.subtle.encrypt(algorithm, key, data) + return Result.ok(new SecretBox(iv, new Uint8Array(cipher))) + } catch (_) {} + + return Result.error([]) + } + public async decrypt(key: Key): Promise> { + const algorithm = {name: "AES-GCM", iv: this.iv} + + try { + const cipher = await window.crypto.subtle.decrypt(algorithm, key, this.cipher) + const buffer = new Uint8Array(cipher) + return Result.ok(buffer) + } catch (_) {} + + return Result.error([]) } public toString() : string { - throw "todo" + const iv = misc.a2b64(this.iv) + const cipher = misc.a2b64(this.cipher) + return `${iv}.${cipher}` } public static fromString(data: string) : Result> { - throw "todo" + const parts = data.split(".") + if (parts.length !== 2) return Result.error([]) + + const iv = misc.b642a(parts[0]) + const cipher = misc.b642a(parts[1]) + + if (iv.error()) return Result.error([]) + if (cipher.error()) return Result.error([]) + + return Result.ok(new SecretBox(iv.unwrap(), cipher.unwrap())) } } diff --git a/test/boxes/symmetric.test.ts b/test/boxes/symmetric.test.ts new file mode 100644 index 0000000..7b44a44 --- /dev/null +++ b/test/boxes/symmetric.test.ts @@ -0,0 +1,57 @@ +import {expect, test} from 'bun:test' + +import * as symmetric from 'boxes/symmetric' + +test('base case', async () => { + const key = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key, data)).expect("Should encrypt the data") + const result = (await box.decrypt(key)).expect("Should decrypt the data") + + expect(result).toEqual(data) +}) + +test('toString and fromString are inverses', async () => { + const key = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key, data)).expect("Should encrypt the data") + const str = box.toString() + const box2 = symmetric.SecretBox.fromString(str).expect("Should parse the string") + expect(box).toEqual(box2) + const plain = (await box2.decrypt(key)).expect("Should decrypt the data") + + expect(plain).toEqual(data) +}) + +test('tampered cipher fails', async () => { + const key = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key, data)).expect("Should encrypt the data") + // @ts-expect-error : This is a test, so it's OK to access private field + box.cipher[0] += 1 + + ;(await box.decrypt(key)).expect_err("Should fail to decrypt the data") +}) + +test('Wrong key fails', async () => { + const key1 = await symmetric.SecretBox.gen_key() + const key2 = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key1, data)).expect("Should encrypt the data") + ;(await box.decrypt(key2)).expect_err("Should fail to decrypt the data") +}) + +test('tampered IV fails', async () => { + const key = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key, data)).expect("Should encrypt the data") + // @ts-expect-error : This is a test, so it's OK to access private field + box.iv[0] += 1 + + ;(await box.decrypt(key)).expect_err("Should fail to decrypt the data") +})