diff --git a/src/boxes/index.ts b/src/boxes/index.ts index 2fda8a4..a30951e 100644 --- a/src/boxes/index.ts +++ b/src/boxes/index.ts @@ -1,5 +1,7 @@ export {PrivateBox} from './asymmetric' export {SecretBox} from './symmetric' export {PwdBox} from './pwd' + export {PrivateWrap} from './private-wrap' export {SecretWrap} from './secret-wrap' +export {PwdWrap} from './pwd-wrap' diff --git a/src/boxes/pwd-wrap.ts b/src/boxes/pwd-wrap.ts new file mode 100644 index 0000000..61dc449 --- /dev/null +++ b/src/boxes/pwd-wrap.ts @@ -0,0 +1,51 @@ +import {Result} from 'result' +import * as misc from 'misc' +import {SecretWrap} from './secret-wrap' +import {pbkdf} from '../pbkdf' + +export class PwdWrap { + private constructor( + private readonly secret_wrap: SecretWrap, + private readonly salt: Uint8Array, + ) {} + + public static async wrap(pwd: string, key_to_wrap: CryptoKey) : Promise> { + const salt = crypto.getRandomValues(new Uint8Array(18)) + const key = await PwdWrap.get_key(pwd, salt) + + const box = await SecretWrap.wrap_key(key, key_to_wrap) + if (box.error()) return Result.error([]) + + return Result.ok(new PwdWrap(box.unwrap(), salt)) + } + + public async unwrap(pwd: string) : Promise> { + const key = await PwdWrap.get_key(pwd, this.salt) + + const unwrapped_key = await this.secret_wrap.unwrap(key) + if (unwrapped_key.error()) return Result.error([]) + + return Result.ok(unwrapped_key.unwrap()) + } + + private static async get_key(pwd: string, salt: Uint8Array) : Promise { + return pbkdf(pwd, salt, ['wrapKey', 'unwrapKey']) + } + + public toString() : string { + const salt = misc.a2b64(this.salt) + const box = this.secret_wrap.toString() + return `${salt}${box}` + } + public static fromString(data: string) : Result { + const salt64 = data.slice(0, 24) + const salt = misc.b642a(salt64) + if (salt.error()) return Result.error([]) + + const box64 = data.slice(24) + const box = SecretWrap.fromString(box64) + if (box.error()) return Result.error([]) + + return Result.ok(new PwdWrap(box.unwrap(), salt.unwrap())) + } +} diff --git a/test/boxes/pwd-wrap.test.ts b/test/boxes/pwd-wrap.test.ts new file mode 100644 index 0000000..2f8b50d --- /dev/null +++ b/test/boxes/pwd-wrap.test.ts @@ -0,0 +1,53 @@ +import {beforeAll, expect, test} from 'bun:test' + +import {PwdWrap} from '../../src/boxes' + +import * as sym from '../../src/boxes/symmetric' +import * as asym from '../../src/boxes/asymmetric' + +let kw_sym!: sym.Key; +let kw_asym!: asym.KeyPair; + +beforeAll(async () => { + kw_sym = await sym.SecretBox.gen_key(true) + expect(kw_sym.extractable).toBe(true) + + kw_asym = await asym.PrivateBox.gen_keypair(true) + expect(kw_asym[0].extractable).toBe(true) +}) + +test('base case', async () => { + const pwd = "password" + const testit = async (key: CryptoKey) => { + console.log(`Testing ${key.type} key with usage ${key.usages}`) + const wrapped = (await PwdWrap.wrap(pwd, key)).expect("Should wrap the key") + const unwrapped = (await wrapped.unwrap(pwd)).expect("Should unwrap the key") + + expect(unwrapped).toEqual(key) + } + + await testit(kw_sym) + await testit(kw_asym[0]) +}) + +test('Fails with wrong password', async () => { + const pwd1 = "AwesomePassword123!" + const pwd2 = "AwesomePassword321!" + expect(pwd1).not.toEqual(pwd2) + + const wrapped = (await PwdWrap.wrap(pwd1, kw_sym)).expect("Should wrap the key") + ;(await wrapped.unwrap(pwd2)).expect_err("Shouldn't unwrap the key with wrong password") +}) + +test('toString and fromString are inverses', async () => { + const pwd = "password" + + const wrapped = (await PwdWrap.wrap(pwd, kw_sym)).expect("Should wrap the key") + + const str = wrapped.toString() + const wrapped2 = PwdWrap.fromString(str).expect("Should unwrap the key") + expect(wrapped2).toEqual(wrapped) + + const unwrapped = (await wrapped.unwrap(pwd)).expect("Should unwrap the key") + expect(unwrapped).toEqual(kw_sym) +})