From ece403476447aacba046315c453a497d9410ddaf Mon Sep 17 00:00:00 2001 From: Pascal Perrenoud Date: Tue, 14 May 2024 22:29:09 +0200 Subject: [PATCH] Implement SecretWrap Closes #6 --- src/boxes/index.ts | 1 + src/boxes/secret-wrap.ts | 85 +++++++++++++++++++++++++++ test/boxes/secret-wrap.test.ts | 102 +++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 src/boxes/secret-wrap.ts create mode 100644 test/boxes/secret-wrap.test.ts diff --git a/src/boxes/index.ts b/src/boxes/index.ts index 5d70dc2..2fda8a4 100644 --- a/src/boxes/index.ts +++ b/src/boxes/index.ts @@ -2,3 +2,4 @@ export {PrivateBox} from './asymmetric' export {SecretBox} from './symmetric' export {PwdBox} from './pwd' export {PrivateWrap} from './private-wrap' +export {SecretWrap} from './secret-wrap' diff --git a/src/boxes/secret-wrap.ts b/src/boxes/secret-wrap.ts new file mode 100644 index 0000000..9937e6b --- /dev/null +++ b/src/boxes/secret-wrap.ts @@ -0,0 +1,85 @@ +import {Result} from 'result' +import * as misc from 'misc' + +export type Key = CryptoKey + +export class SecretWrap { + private constructor( + private readonly wrapped_key: Uint8Array, + private readonly algorithm: Algorithm, + private readonly usage: KeyUsage[], + private readonly format: KeyFormat, + private readonly iv: Uint8Array) {} + + public static async gen_key(extractable: boolean = false) : Promise { + return crypto.subtle.generateKey( + {name: 'AES-GCM', length: 256}, + extractable, + ['wrapKey', 'unwrapKey'] + ) + } + + public static async wrap_key(wrapping_key: Key, key_to_wrap: CryptoKey) : Promise> { + const format = key_to_wrap.type === "secret" ? "raw" : "pkcs8" + const iv = crypto.getRandomValues(new Uint8Array(12)) + + try { + const wrapped_key = await crypto.subtle.wrapKey( + format, + key_to_wrap, + wrapping_key, + { + name: 'AES-GCM', + iv, + } + ) + + return Result.ok(new SecretWrap(new Uint8Array(wrapped_key), key_to_wrap.algorithm, key_to_wrap.usages, format, iv)) + } catch (_) {} + + return Result.error([]) + } + + public async unwrap(wrapping_key: Key) : Promise> { + try { + const key = await crypto.subtle.unwrapKey( + this.format, + this.wrapped_key, + wrapping_key, + { + name: 'AES-GCM', + iv: this.iv, + }, + this.algorithm, + true, + this.usage, + ) + + return Result.ok(key) + } catch (_) {} + + return Result.error([]) + } + + public toString() : string { + const wrapped_key = misc.a2b64(this.wrapped_key) + const iv = misc.a2b64(this.iv) + return JSON.stringify({ + wrapped_key, + algorithm: this.algorithm, + usage: this.usage, + format: this.format, + iv, + }) + } + public static fromString(s: string) : Result { + const {iv: iv64, wrapped_key: wrapped_key64, algorithm, usage, format}: {wrapped_key: string, algorithm: Algorithm, usage: KeyUsage[], format: KeyFormat, iv: string} = JSON.parse(s) + + const iv = misc.b642a(iv64) + if (iv.error()) return Result.error([]) + const wrapped_key = misc.b642a(wrapped_key64) + if (wrapped_key.error()) return Result.error([]) + + return Result.ok(new SecretWrap(wrapped_key.unwrap(), algorithm, usage, format, iv.unwrap())) + } +} diff --git a/test/boxes/secret-wrap.test.ts b/test/boxes/secret-wrap.test.ts new file mode 100644 index 0000000..07fd5d7 --- /dev/null +++ b/test/boxes/secret-wrap.test.ts @@ -0,0 +1,102 @@ +import {beforeAll, expect, test} from 'bun:test' + +import {SecretWrap, type Key} from 'boxes/secret-wrap' + +import * as sym from 'boxes/symmetric' +import * as asym from 'boxes/asymmetric' +import * as signature from 'signature' +import * as pwrap from 'boxes/private-wrap' + +let k1!: Key; +let k2!: Key; + +let kw_wrap!: sym.Key; +let kw_sym!: sym.Key; +let kw_sig!: signature.KeyPair; +let kw_asym!: asym.KeyPair; +let kw_priv!: pwrap.KeyPair; + +let kw_wrap_non!: sym.Key; +let kw_sym_non!: sym.Key; +let kw_sig_non!: signature.KeyPair; +let kw_asym_non!: asym.KeyPair; +let kw_priv_non!: pwrap.KeyPair; + +beforeAll(async () => { + k1 = await SecretWrap.gen_key(false) + k2 = await SecretWrap.gen_key(true) + + expect(k1.extractable).toBe(false) + expect(k2.extractable).toBe(true) + + kw_wrap = await sym.SecretBox.gen_key(true) + expect(kw_wrap.extractable).toBe(true) + kw_asym = await asym.PrivateBox.gen_keypair(true) + expect(kw_asym[0].extractable).toBe(true) + kw_priv = await pwrap.PrivateWrap.gen_key(true) + expect(kw_priv[0].extractable).toBe(true) + kw_sym = await sym.SecretBox.gen_key(true) + expect(kw_sym.extractable).toBe(true) + kw_sig = await signature.gen_keypair(true) + expect(kw_sig[0].extractable).toBe(true) + + kw_wrap_non = await sym.SecretBox.gen_key(false) + expect(kw_wrap_non.extractable).toBe(false) + kw_asym_non = await asym.PrivateBox.gen_keypair(false) + expect(kw_asym_non[0].extractable).toBe(false) + kw_priv_non = await pwrap.PrivateWrap.gen_key(false) + expect(kw_priv_non[0].extractable).toBe(false) + kw_sym_non = await sym.SecretBox.gen_key(false) + expect(kw_sym_non.extractable).toBe(false) + kw_sig_non = await signature.gen_keypair(false) + expect(kw_sig_non[0].extractable).toBe(false) +}) + +test('base case', async () => { + const testit = async (key: CryptoKey) => { + console.log(`Testing ${key.type} key with usage ${key.usages}`) + const wrapped = (await SecretWrap.wrap_key(k1, key)).expect("Should wrap the key") + const unwrapped = (await wrapped.unwrap(k1)).expect("Should unwrap the key") + expect(unwrapped).toEqual(key) + } + + await testit(kw_wrap) + await testit(kw_asym[0]) + await testit(kw_priv[0]) + await testit(kw_sym) + await testit(kw_sig[0]) +}) +test("toString and fromString and inverses", async () => { + const testit = async (key: CryptoKey) => { + console.log(`Testing ${key.type} key with usage ${key.usages}`) + const wrapped = (await SecretWrap.wrap_key(k1, key)).expect("Should wrap the key") + const wrapped_str = wrapped.toString() + const unwrapped = (SecretWrap.fromString(wrapped_str)).expect("Should parse the key") + expect(unwrapped).toEqual(wrapped) + } + + await testit(kw_wrap) + await testit(kw_asym[0]) + await testit(kw_priv[0]) + await testit(kw_sym) + await testit(kw_sig[0]) +}) +test("Can't unwrap with wrong key", async () => { + const wrapped = (await SecretWrap.wrap_key(k1, kw_wrap)).expect("Should wrap the key") + ;(await wrapped.unwrap(k2)).expect_err("Shouldn't unwrap with wrong key") +}) +test("Can't wrap if key is not extractable", async () => { + ;(await SecretWrap.wrap_key(k1, kw_wrap_non)).expect_err("Shouldn't wrap if key is not extractable") + ;(await SecretWrap.wrap_key(k1, kw_asym_non[0])).expect_err("Shouldn't wrap if key is not extractable") + ;(await SecretWrap.wrap_key(k1, kw_priv_non[0])).expect_err("Shouldn't wrap if key is not extractable") + ;(await SecretWrap.wrap_key(k1, kw_sym_non)).expect_err("Shouldn't wrap if key is not extractable") + ;(await SecretWrap.wrap_key(k1, kw_sig_non[0])).expect_err("Shouldn't wrap if key is not extractable") +}) +test("tampered IV", async () => { + const wrapped = (await SecretWrap.wrap_key(k1, kw_wrap)).expect("Should wrap the key") + + // @ts-expect-error + wrapped.iv[0] += 1 + + ;(await wrapped.unwrap(k1)).expect_err("Shouldn't unwrap with tampered IV") +})