diff --git a/asymmetric.ts b/asymmetric.ts deleted file mode 100644 index e69de29..0000000 diff --git a/pwd.ts b/pwd.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/boxes/asymmetric.ts b/src/boxes/asymmetric.ts new file mode 100644 index 0000000..be8e4db --- /dev/null +++ b/src/boxes/asymmetric.ts @@ -0,0 +1,74 @@ +import {Result} from 'rust' +import * as misc from 'misc' +import logger from 'log' + +export type PubKey = CryptoKey +export type PrivKey = CryptoKey +export type KeyPair = [PrivKey, PubKey] + +const algorithm: RsaOaepParams = {name: "RSA-OAEP"} +const log = logger("crypto:boxes:private") + +export class PrivateBox { + readonly _phantom!: T; + + private constructor( + private readonly cipher: Uint8Array + ) {} + + public static async gen_keypair(extractable: boolean = false): Promise { + log.trace('generate keypair') + log.debug('extractable :', extractable ? 'yes' : 'no') + + const keys = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: {name: "SHA-512"} + }, + extractable, + ["encrypt", "decrypt"] + ) + + return [keys.privateKey, keys.publicKey] + } + + public static async encrypt(key: PubKey, data: Uint8Array): Promise>> { + log.trace('encrypt') + + try { + const cipher = await window.crypto.subtle.encrypt(algorithm, key, data) + return Result.ok(new PrivateBox(new Uint8Array(cipher))) + } catch(e) { + log.warn('encryption failed') + log.debug(`error : ${e}`) + } + + return Result.error([]) + } + public async decrypt(key: PrivKey): Promise> { + log.trace('decrypt') + + try { + const plain = await window.crypto.subtle.decrypt(algorithm, key, this.cipher) + return Result.ok(new Uint8Array(plain)) + } catch(e) { + log.warn('decryption failed') + log.debug(`error : ${e}`) + } + + return Result.error([]) + } + + public toString() : string { + log.trace('toString') + return misc.a2b64(new Uint8Array(this.cipher)) + } + public static fromString(data: string) : Result> { + log.trace('fromString') + const res = misc.b642a(data) + if (res.is_err()) return Result.error([]) + return Result.ok(new PrivateBox(res.unwrap())) + } +} diff --git a/src/boxes/private-wrap.ts b/src/boxes/private-wrap.ts new file mode 100644 index 0000000..8250fed --- /dev/null +++ b/src/boxes/private-wrap.ts @@ -0,0 +1,127 @@ +import {Result} from 'rust' +import * as misc from 'misc' +import logger from 'log' + +export type PubKey = CryptoKey +export type PrivKey = CryptoKey +export type KeyPair = [PrivKey, PubKey] + +const algorithm: RsaOaepParams = {name: "RSA-OAEP"} +const log = logger("crypto:boxes:private-wrap") + +export class PrivateWrap { + private constructor( + private readonly wrapped_key: Uint8Array, + private readonly algorithm: Algorithm, + private readonly usage: KeyUsage[], + private readonly format: KeyFormat + ) {} + + /** + * 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 { + log.trace('generate keypair') + log.debug('extractable :', extractable ? 'yes' : 'no') + + 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> { + log.trace('wrap key') + + 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 (e) { + log.warn('wrap failed') + log.debug(`error : ${e}`) + } + + return Result.error([]) + } + + /** + * Unwrap a key using a private key + * @param privkey used to unwrap + */ + public async unwrap(privkey: PrivKey) : Promise> { + log.trace('unwrap key') + + 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(e) { + log.warn('unwrap failed') + log.debug(`error : ${e}`) + } + + return Result.error([]) + } + + public toString() : string { + log.trace('toString') + + 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 { + log.trace('fromString') + + 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.is_err()) return Result.error([]) + + return Result.ok(new PrivateWrap(key.unwrap(), algorithm, usage, format)) + } catch (e) { + log.warn('parsing failed') + log.debug(`error : ${e}`) + } + + return Result.error([]) + } +} diff --git a/src/boxes/pwd-wrap.ts b/src/boxes/pwd-wrap.ts new file mode 100644 index 0000000..16f1f56 --- /dev/null +++ b/src/boxes/pwd-wrap.ts @@ -0,0 +1,60 @@ +import {Result} from 'rust' +import * as misc from 'misc' +import {SecretWrap} from './secret-wrap' +import {pbkdf} from '../pbkdf' +import logger from 'log' + +const log = logger('crypto:boxes:pwd-wrap') + +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> { + log.trace('wrap key') + + 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.is_err()) return Result.error([]) + + return Result.ok(new PwdWrap(box.unwrap(), salt)) + } + + public async unwrap(pwd: string) : Promise> { + log.trace('unwrap key') + + const key = await PwdWrap.get_key(pwd, this.salt) + + const unwrapped_key = await this.secret_wrap.unwrap(key) + if (unwrapped_key.is_err()) 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 { + log.trace('fromString') + + const salt64 = data.slice(0, 24) + const salt = misc.b642a(salt64) + if (salt.is_err()) return Result.error([]) + + const box64 = data.slice(24) + const box = SecretWrap.fromString(box64) + if (box.is_err()) return Result.error([]) + + return Result.ok(new PwdWrap(box.unwrap(), salt.unwrap())) + } +} diff --git a/src/boxes/pwd.ts b/src/boxes/pwd.ts new file mode 100644 index 0000000..95df701 --- /dev/null +++ b/src/boxes/pwd.ts @@ -0,0 +1,54 @@ +import {Result} from 'rust' +import * as misc from 'misc' +import {SecretBox} from './symmetric' +import {pbkdf} from '../pbkdf' +import logger from 'log' + +const log = logger('crypto:boxes:pwd') + +export class PwdBox { + + private constructor( + private readonly secret_box: SecretBox, + private readonly salt: Uint8Array + ) {} + + public static async encrypt(pwd: string, data: Uint8Array): Promise> { + log.trace('encrypt') + + const salt = crypto.getRandomValues(new Uint8Array(18)) + const key = await PwdBox.get_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> { + log.trace('decrypt') + const key = await PwdBox.get_key(pwd, this.salt) + return this.secret_box.decrypt(key) + } + + private static async get_key(pwd: string, salt: Uint8Array) : Promise { + return pbkdf(pwd, salt, ["encrypt", "decrypt"]) + } + + public toString() : string { + const salt = misc.a2b64(this.salt) + const box = this.secret_box.toString() + return `${salt}${box}` + } + public static fromString(data: string) : Result> { + log.trace('fromString') + + const salt64 = data.slice(0, 24) + const salt = misc.b642a(salt64) + if (salt.is_err()) return Result.error([]) + + const box64 = data.slice(24) + const box = SecretBox.fromString(box64) + if (box.is_err()) return Result.error([]) + + return Result.ok(new PwdBox(box.unwrap(), salt.unwrap())) + } +} diff --git a/src/boxes/secret-wrap.ts b/src/boxes/secret-wrap.ts new file mode 100644 index 0000000..fbd76be --- /dev/null +++ b/src/boxes/secret-wrap.ts @@ -0,0 +1,110 @@ +import {Result} from 'rust' +import * as misc from 'misc' +import logger from 'log' + +export type Key = CryptoKey + +const log = logger('crypto:boxes:secret-wrap') + +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 { + log.trace('Generating key') + log.debug('Extractable :', extractable ? 'yes' : 'no') + + 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> { + log.trace('wrap key') + + 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 (e) { + log.warn('Wrapping failed') + log.debug(`Error : ${e}`) + } + + return Result.error([]) + } + + public async unwrap(wrapping_key: Key) : Promise> { + log.trace('unwrap key') + + 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 (e) { + log.warn('Unwrapping failed') + log.debug(`Error : ${e}`) + } + + 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 { + log.trace('fromString') + + try { + 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.is_err()) return Result.error([]) + const wrapped_key = misc.b642a(wrapped_key64) + if (wrapped_key.is_err()) return Result.error([]) + + return Result.ok(new SecretWrap(wrapped_key.unwrap(), algorithm, usage, format, iv.unwrap())) + } catch(e) { + log.warn('Parsing fromString failed') + log.debug(`Error : ${e}`) + } + + return Result.error([]) + } +} diff --git a/src/boxes/symmetric.ts b/src/boxes/symmetric.ts new file mode 100644 index 0000000..e2d64da --- /dev/null +++ b/src/boxes/symmetric.ts @@ -0,0 +1,87 @@ +import {Result} from 'rust' +import * as misc from 'misc' +import logger from 'log' + +export type Key = CryptoKey + +const log = logger("crypto:boxes:secret") + +export class SecretBox { + private readonly phantom!: T + + private constructor( + private readonly iv: Uint8Array, + private readonly cipher: Uint8Array + ) {} + + public static async gen_key(extractable: boolean = false): Promise { + log.trace('Generate key') + log.debug('Extractable :', extractable ? 'yes' : 'no') + + return window.crypto.subtle.generateKey( + { + name: "AES-GCM", + length: 256, + }, + extractable, + ["encrypt", "decrypt"] + ) + } + + public static async encrypt(key: Key, data: Uint8Array): Promise>> { + log.trace('Encrypt data') + + const iv = window.crypto.getRandomValues(new Uint8Array(12)) + const algorithm = {name: "AES-GCM", iv} + + try { + const cipher = await window.crypto.subtle.encrypt(algorithm, key, data) + return Result.ok(new SecretBox(iv, new Uint8Array(cipher))) + } catch (e) { + log.warn('Encryption failed') + log.debug(`Error : ${e}`) + } + + return Result.error([]) + } + public async decrypt(key: Key): Promise> { + log.trace('Decrypt data') + + const algorithm = {name: "AES-GCM", iv: this.iv} + + try { + const cipher = await window.crypto.subtle.decrypt(algorithm, key, this.cipher) + const buffer = new Uint8Array(cipher) + return Result.ok(buffer) + } catch (e) { + log.warn('Decryption failed') + log.debug(`Error : ${e}`) + } + + return Result.error([]) + } + + public toString() : string { + const iv = misc.a2b64(this.iv) + const cipher = misc.a2b64(this.cipher) + return `${iv}.${cipher}` + } + public static fromString(data: string) : Result> { + log.trace('fromString') + + const parts = data.split(".") + if (parts.length !== 2) { + log.warn("Invalid secret box, part count doesn't match") + log.debug(`Count : ${parts.length}`) + return Result.error([]) + } + + const iv = misc.b642a(parts[0]) + const cipher = misc.b642a(parts[1]) + + if (iv.is_err()) return Result.error([]) + if (cipher.is_err()) return Result.error([]) + + return Result.ok(new SecretBox(iv.unwrap(), cipher.unwrap())) + } +} diff --git a/symmetric.ts b/symmetric.ts deleted file mode 100644 index e69de29..0000000 diff --git a/test/boxes/asymmetric.test.ts b/test/boxes/asymmetric.test.ts new file mode 100644 index 0000000..543a8d7 --- /dev/null +++ b/test/boxes/asymmetric.test.ts @@ -0,0 +1,58 @@ +import {beforeAll, expect, test} from 'bun:test' + +import {type KeyPair, PrivateBox} from 'boxes/asymmetric' + +let k1!: KeyPair; +let k2!: KeyPair; + +beforeAll(async () => { + k1 = await PrivateBox.gen_keypair(false) + k2 = await PrivateBox.gen_keypair(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) +}) + +test('base case', async () => { + const [priv, pub] = k1 + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await PrivateBox.encrypt(pub, data)).expect("Should encrypt the data") + const result = (await box.decrypt(priv)).expect("Should decrypt the data") + + expect(result).toEqual(data) +}) + +test('toString and fromString are inverses', async () => { + const [priv, pub] = k1 + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await PrivateBox.encrypt(pub, data)).expect("Should encrypt the data") + const str = box.toString() + const box2 = PrivateBox.fromString(str).expect("Should parse the string") + expect(box).toEqual(box2) + const plain = (await box2.decrypt(priv)).expect("Should decrypt the data") + + expect(plain).toEqual(data) +}) + +test('Tampered cipher fails', async () => { + const [priv, pub] = k1 + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await PrivateBox.encrypt(pub, data)).expect("Should encrypt the data") + // @ts-expect-error : Bypass privacy for test + box.cipher[0] += 1 + ;(await box.decrypt(priv)).expect_err("Should fail to decrypt the data") +}) + +test('Wrong pubkey should fail', async () => { + const [_priv, pub] = k1 + const [priv, _pub] = k2 + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await PrivateBox.encrypt(pub, data)).expect("Should encrypt the data") + ;(await box.decrypt(priv)).expect_err("Should fail to decrypt the data") +}) 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") +}) 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) +}) 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") +}) 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") +}) diff --git a/test/boxes/symmetric.test.ts b/test/boxes/symmetric.test.ts new file mode 100644 index 0000000..7b44a44 --- /dev/null +++ b/test/boxes/symmetric.test.ts @@ -0,0 +1,57 @@ +import {expect, test} from 'bun:test' + +import * as symmetric from 'boxes/symmetric' + +test('base case', async () => { + const key = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key, data)).expect("Should encrypt the data") + const result = (await box.decrypt(key)).expect("Should decrypt the data") + + expect(result).toEqual(data) +}) + +test('toString and fromString are inverses', async () => { + const key = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key, data)).expect("Should encrypt the data") + const str = box.toString() + const box2 = symmetric.SecretBox.fromString(str).expect("Should parse the string") + expect(box).toEqual(box2) + const plain = (await box2.decrypt(key)).expect("Should decrypt the data") + + expect(plain).toEqual(data) +}) + +test('tampered cipher fails', async () => { + const key = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key, data)).expect("Should encrypt the data") + // @ts-expect-error : This is a test, so it's OK to access private field + box.cipher[0] += 1 + + ;(await box.decrypt(key)).expect_err("Should fail to decrypt the data") +}) + +test('Wrong key fails', async () => { + const key1 = await symmetric.SecretBox.gen_key() + const key2 = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key1, data)).expect("Should encrypt the data") + ;(await box.decrypt(key2)).expect_err("Should fail to decrypt the data") +}) + +test('tampered IV fails', async () => { + const key = await symmetric.SecretBox.gen_key() + const data = new Uint8Array([1, 2, 3, 4, 5]) + + const box = (await symmetric.SecretBox.encrypt(key, data)).expect("Should encrypt the data") + // @ts-expect-error : This is a test, so it's OK to access private field + box.iv[0] += 1 + + ;(await box.decrypt(key)).expect_err("Should fail to decrypt the data") +})