From f30516bd2c9a9fa1b6cf56de045955de421c2745 Mon Sep 17 00:00:00 2001 From: Pascal Perrenoud Date: Tue, 14 May 2024 15:58:35 +0200 Subject: [PATCH] Implement pwd box --- src/boxes/pwd.ts | 61 +++++++++++++++++++++++++++++++++++++----- test/boxes/pwd.test.ts | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 test/boxes/pwd.test.ts diff --git a/src/boxes/pwd.ts b/src/boxes/pwd.ts index 7004da8..dea58ff 100644 --- a/src/boxes/pwd.ts +++ b/src/boxes/pwd.ts @@ -1,17 +1,64 @@ -import type {Result} from 'result' +import {Result} from 'result' +import * as misc from 'misc' +import {SecretBox} from './symmetric' export class PwdBox { - public static encrypt(pwd: string, data: Uint8Array) : PwdBox { - throw "todo" + private readonly secret_box: SecretBox + private readonly salt: Uint8Array + + private constructor(secret_box: SecretBox, salt: Uint8Array) { + this.secret_box = secret_box + this.salt = salt } - public decrypt(pwd: string) : Result { - throw "todo" + + public static async encrypt(pwd: string, data: Uint8Array): Promise> { + const salt = crypto.getRandomValues(new Uint8Array(18)) + const key = await PwdBox.gen_key(pwd, salt) + + const box = (await SecretBox.encrypt(key, data)).unwrap() // I just created the key, I control it + + return new PwdBox(box, salt) + } + public async decrypt(pwd: string): Promise> { + const key = await PwdBox.gen_key(pwd, this.salt) + return this.secret_box.decrypt(key) + } + + private static async gen_key(pwd: string, salt: Uint8Array) : Promise { + const keyMaterial = await window.crypto.subtle.importKey( + "raw", + new TextEncoder().encode(pwd), + "PBKDF2", + false, + ["deriveBits", "deriveKey"], + ) + + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + iterations: 250_000, + hash: "SHA-512", + salt, + }, + keyMaterial, + {name: "AES-GCM", length: 256}, + false, + ["encrypt", "decrypt"], + ) } public toString() : string { - throw "todo" + const salt = misc.a2b64(this.salt) + const box = this.secret_box.toString() + return `${salt}${box}` } public static fromString(data: string) : Result> { - throw "todo" + const salt = misc.b642a(data.slice(0, 24)) + if (salt.error()) return Result.error([]) + + const box = SecretBox.fromString(data.slice(24)) + if (box.error()) return Result.error([]) + + return Result.ok(new PwdBox(box.unwrap(), salt.unwrap())) } } diff --git a/test/boxes/pwd.test.ts b/test/boxes/pwd.test.ts new file mode 100644 index 0000000..3ddabc5 --- /dev/null +++ b/test/boxes/pwd.test.ts @@ -0,0 +1,52 @@ +import {expect, test} from 'bun:test' + +import * as pwd from 'boxes/pwd' + +test('base case', async () => { + const password = "AwesomePassword123!" + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = await pwd.PwdBox.encrypt(password, data) + const result = (await box.decrypt(password)).expect("Should decrypt the data") + + expect(result).toEqual(data) +}) + +test('wrong password', async () => { + const password1 = "AwesomePassword123!" + const password2 = "AwesomePassword321!" + expect(password1).not.toEqual(password2) + + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = await pwd.PwdBox.encrypt(password1, data) + + ;(await box.decrypt(password2)).expect_err("Should not decrypt the data with the wrong password") +}) + +test('toString and fromString are inverses', async () => { + const password = "AwesomePassword123!" + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = await pwd.PwdBox.encrypt(password, data) + + const str = box.toString() + const box2 = pwd.PwdBox.fromString(str).expect("Should be able to parse the string") + expect(box2).toEqual(box) + + const result = (await box2.decrypt(password)).expect("Should decrypt the data") + + expect(result).toEqual(data) +}) + +test('tampered salt should fail', async () => { + const password = "AwesomePassword123!" + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = await pwd.PwdBox.encrypt(password, data) + + // @ts-expect-error : I know salt is private, but I want to test it + box.salt[0] += 1 + + ;(await box.decrypt(password)).expect_err("Should not decrypt the data with a tampered salt") +})