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..3c1bd69 --- /dev/null +++ b/src/boxes/private-wrap.ts @@ -0,0 +1,109 @@ +import {Result} from 'result' +import * as misc from 'misc' + +export type PubKey = CryptoKey +export type PrivKey = CryptoKey + +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<[PrivKey, PubKey]> { + 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/src/signature.ts b/src/signature.ts index 1168462..1282671 100644 --- a/src/signature.ts +++ b/src/signature.ts @@ -11,16 +11,20 @@ const algorithm: EcdsaParams = { /** * Create a new keypair for signing * @param extractable if the keys must be extractable or not + * @param canWrap if the keys can be used for wrapping WARNING : it is a bad idea to use the same key for signing and wrapping * @return [privkey, pubkey] keys */ -export async function gen_keypair(extractable: boolean = false) : Promise<[PrivKey, PubKey]> { - let key = await window.crypto.subtle.generateKey( +export async function gen_keypair(extractable: boolean = false, canWrap: boolean = false) : Promise<[PrivKey, PubKey]> { + const usage: KeyUsage[] = ['sign', 'verify'] + if(canWrap) usage.push('wrapKey', 'unwrapKey') + + const key = await window.crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-521" } as EcKeyGenParams, extractable, - ['sign', 'verify'] + usage ) return [key.privateKey, key.publicKey] diff --git a/test/boxes/private-wrap.test.ts b/test/boxes/private-wrap.test.ts new file mode 100644 index 0000000..a0379ce --- /dev/null +++ b/test/boxes/private-wrap.test.ts @@ -0,0 +1,85 @@ +import {beforeAll, expect, test} from 'bun:test' + +import {PrivateWrap, type PubKey, type PrivKey} from 'boxes/private-wrap' + +import * as sym from 'boxes/symmetric' +import * as signature from 'signature' + +let k1!: [PrivKey, PubKey]; +let k2!: [PrivKey, PubKey]; +let kw_sym!: sym.Key; +let kw_sig!: [signature.PrivKey, signature.PubKey]; + +let kw_sym_non!: sym.Key; +let kw_sig_non!: [signature.PrivKey, signature.PubKey]; + +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") +})