From 528ccbdad78dfaf656a4e3d38651d61fab4c8c7a Mon Sep 17 00:00:00 2001 From: Pascal Perrenoud Date: Tue, 14 May 2024 22:24:33 +0200 Subject: [PATCH] Implement PrivateWrap Closes #7 --- src/boxes/index.ts | 1 + src/boxes/private-wrap.ts | 110 ++++++++++++++++++++++++++++++++ test/boxes/private-wrap.test.ts | 86 +++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 src/boxes/private-wrap.ts create mode 100644 test/boxes/private-wrap.test.ts diff --git a/src/boxes/index.ts b/src/boxes/index.ts index 5617fbd..5d70dc2 100644 --- a/src/boxes/index.ts +++ b/src/boxes/index.ts @@ -1,3 +1,4 @@ export {PrivateBox} from './asymmetric' export {SecretBox} from './symmetric' export {PwdBox} from './pwd' +export {PrivateWrap} from './private-wrap' diff --git a/src/boxes/private-wrap.ts b/src/boxes/private-wrap.ts new file mode 100644 index 0000000..fe67e5b --- /dev/null +++ b/src/boxes/private-wrap.ts @@ -0,0 +1,110 @@ +import {Result} from 'result' +import * as misc from 'misc' + +export type PubKey = CryptoKey +export type PrivKey = CryptoKey +export type KeyPair = [PrivKey, PubKey] + +const algorithm: RsaOaepParams = {name: "RSA-OAEP"} + +export class PrivateWrap { + private readonly wrapped_key: Uint8Array + private readonly usage: KeyUsage[] + private readonly algorithm: Algorithm + private readonly format: KeyFormat + + private constructor(key: Uint8Array, algorithm: Algorithm, usage: KeyUsage[], format: KeyFormat) { + this.wrapped_key = key + this.algorithm = algorithm + this.usage = usage + this.format = format + } + + /** + * Create a new keypair for signing + * @param extractable if the keys must be extractable or not + * @return [privkey, pubkey] keys + */ + public static async gen_key(extractable: boolean = false) : Promise { + let key = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: {name: "SHA-512"}, + }, + extractable, + ["wrapKey", "unwrapKey"] + ) + + return [key.privateKey, key.publicKey] + } + + /** + * Wrap a key using a public key + * + * Kinda makes sense, but it can't wrap an RSA encryption key + * @param pubkey used to wrap + * @param key to wrap + * @return an error if the key is not extractable + */ + public static async wrap(pubkey: PubKey, key: CryptoKey) : Promise> { + const format = key.type === "secret" ? "raw" : "pkcs8" + + try { + const wrapped = await window.crypto.subtle.wrapKey( + format, + key, + pubkey, + algorithm + ) + + return Result.ok(new PrivateWrap(new Uint8Array(wrapped), key.algorithm, key.usages, format)) + } catch (_) {} + + return Result.error([]) + } + + /** + * Unwrap a key using a private key + * @param privkey used to unwrap + */ + public async unwrap(privkey: PrivKey) : Promise> { + try { + const key = await window.crypto.subtle.unwrapKey( + this.format, + this.wrapped_key, + privkey, + algorithm, + this.algorithm, + true, + this.usage + ) + return Result.ok(key) + } catch(_) {} + + return Result.error([]) + } + + public toString() : string { + const wrapped_key = misc.a2b64(this.wrapped_key) + return JSON.stringify({ + wrapped_key, + algorithm: this.algorithm, + usage: this.usage, + format: this.format, + }) + } + public static fromString(data: string) : Result { + try { + const {wrapped_key: wrapped_key64, algorithm, usage, format}: {wrapped_key: string, algorithm: Algorithm, usage: KeyUsage[], format: KeyFormat} = JSON.parse(data) + + const key = misc.b642a(wrapped_key64) + if (key.error()) return Result.error([]) + + return Result.ok(new PrivateWrap(key.unwrap(), algorithm, usage, format)) + } catch (_) {} + + return Result.error([]) + } +} diff --git a/test/boxes/private-wrap.test.ts b/test/boxes/private-wrap.test.ts new file mode 100644 index 0000000..17b4660 --- /dev/null +++ b/test/boxes/private-wrap.test.ts @@ -0,0 +1,86 @@ +import {beforeAll, expect, test} from 'bun:test' + +import {PrivateWrap, type KeyPair} from 'boxes/private-wrap' + +import * as sym from 'boxes/symmetric' +import * as signature from 'signature' + +let k1!: KeyPair; +let k2!: KeyPair; + +let kw_sym!: sym.Key; +let kw_sig!: signature.KeyPair; + +let kw_sym_non!: sym.Key; +let kw_sig_non!: signature.KeyPair; + +beforeAll(async () => { + k1 = await PrivateWrap.gen_key(false) + k2 = await PrivateWrap.gen_key(true) + + expect(k1[0].extractable).toBe(false) + expect(k1[1].extractable).toBe(true) + expect(k2[0].extractable).toBe(true) + expect(k2[1].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_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 [priv, pub] = k1 + expect(pub.type).toBe("public") + + + const sym = (await PrivateWrap.wrap(pub, kw_sym)).expect("Should wrap the sym key") + const rsym = (await sym.unwrap(priv)).expect("Should unwrap the sym key") + expect(rsym).toEqual(kw_sym) + + const sig = (await PrivateWrap.wrap(pub, kw_sig[0])).expect("Should wrap the signature key") + const rsig = (await sig.unwrap(priv)).expect("Should unwrap the signature key") + expect(rsig).toEqual(kw_sig[0]) +}) +test('toString and fromString and inverses', async () => { + const [_priv, pub] = k1 + + const sym = (await PrivateWrap.wrap(pub, kw_sym)).expect("Should wrap the sym key") + const sym_str = sym.toString() + const rsym = (PrivateWrap.fromString(sym_str)).expect("Should parse the sym key") + expect(rsym).toEqual(sym) + + const sig = (await PrivateWrap.wrap(pub, kw_sig[0])).expect("Should wrap the signature key") + const sig_str = sig.toString() + const rsig = (PrivateWrap.fromString(sig_str)).expect("Should parse the signature key") + expect(rsig).toEqual(sig) +}) +test("Can't wrap with private key", async () => { + const [priv, _pub] = k1 + + ;(await PrivateWrap.wrap(priv, kw_sym)).expect_err("Shouldn't wrap with private key") +}) +test("Can't unwrap with public key", async () => { + const [_priv, pub] = k1 + const sym = (await PrivateWrap.wrap(pub, kw_sym)).expect("Should wrap the sym key") + + ;(await sym.unwrap(pub)).expect_err("Shouldn't unwrap with public key") +}) +test("Can't unwrap with wrong private key", async () => { + const [_priv, pub] = k1 + const sym = (await PrivateWrap.wrap(pub, kw_sym)).expect("Should wrap the sym key") + + const [priv, _pub] = k2 + ;(await sym.unwrap(priv)).expect_err("Shouldn't unwrap with wrong private key") +}) +test("Can't wrap if not extractable", async () => { + const [_priv, pub] = k1 + + ;(await PrivateWrap.wrap(pub, kw_sym_non)).expect_err("Shouldn't wrap if not extractable") + ;(await PrivateWrap.wrap(pub, kw_sig_non[0])).expect_err("Shouldn't wrap if not extractable") +})