diff --git a/src/misc.ts b/src/misc.ts index 0c84929..b9e41d2 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -35,6 +35,28 @@ export async function pubkey_fromString(pubkey_str: string, usage: Usage): Promi } } +export async function pubkey_toBytes(pubkey: CryptoKey): Promise { + log.trace('pubkey_toBytes') + return new Uint8Array(await crypto.subtle.exportKey('raw', pubkey)) +} +export async function pubkey_fromBytes(pubkey: Uint8Array, usage: Usage): Promise { + log.trace('pubkey_fromBytes') + + try { + return await crypto.subtle.importKey( + 'raw', + pubkey, + usage === Usage.ecdh ? ECDH_PARAMETERS() : SIGNATURE_KEY(), + true, + usage === Usage.ecdh ? [] : ['verify'] + ) + } catch (e) { + log.warn('Failed to import public key') + log.debug('Error :', e) + return null + } +} + export function payload_fromString(text: string): Uint8Array { return new TextEncoder().encode(text) } diff --git a/src/private-box.ts b/src/private-box.ts index 1c947d7..03e0915 100644 --- a/src/private-box.ts +++ b/src/private-box.ts @@ -3,6 +3,7 @@ import SecretBox from './secret-box' import {DHusage, ecdh} from './kdf' import {ECDH_PARAMETERS} from './const' import * as misc from './misc' +import {pubkey_fromBytes, pubkey_toBytes, Usage} from './misc' const log = logger('crypto:private-box') @@ -57,6 +58,32 @@ export default class PrivateBox { return new PrivateBox(pubkey, box) } + public async toBytes(): Promise { + const pubkey = await pubkey_toBytes(this.pubkey) + const box = this.box.toBytes() + return new Uint8Array([pubkey.length, ...pubkey, ...box]) + } + public static async fromBytes(data: Uint8Array): Promise { + const length = data.at(0) + if (length === undefined) { + log.warn('Data is invalid') + return null + } + + const pubkey_buff = data.slice(1, 1 + length) + if (pubkey_buff.length !== length) { + log.warn('Data too short to contain pubkey') + return null + } + const pubkey = await pubkey_fromBytes(pubkey_buff, Usage.ecdh) + if (pubkey === null) return null + + const box = SecretBox.fromBytes(data.slice(1 + length)) + if (box === null) return null + + return new this(pubkey, box) + } + public static async pubkey_toString(pubkey: CryptoKey): Promise { return await misc.pubkey_toString(pubkey) } diff --git a/src/pwd-box.ts b/src/pwd-box.ts index 6cf9a9a..0da236d 100644 --- a/src/pwd-box.ts +++ b/src/pwd-box.ts @@ -5,6 +5,8 @@ import {a2b64, b642a} from 'misc' const log = logger('crypto:pwd-box') +const SALT_byte_length = 16 + export default class PwdBox { private constructor( private readonly box: SecretBox, @@ -20,7 +22,7 @@ export default class PwdBox { public static async encrypt(data: Uint8Array, pwd: string, context?: string): Promise { log.trace('encrypt') - const salt = crypto.getRandomValues(new Uint8Array(16)) + const salt = crypto.getRandomValues(new Uint8Array(SALT_byte_length)) const k = await PwdBox.derive(pwd, salt, context) if (k === null) return null @@ -58,4 +60,22 @@ export default class PwdBox { return new PwdBox(box, salt) } + + public toBytes(): Uint8Array { + const box = this.box.toBytes() + return new Uint8Array([...this.salt, ...box]) + } + public static fromBytes(data: Uint8Array): PwdBox | null { + if (data.length <= SALT_byte_length) { + log.warn('Data is smaller than salt size') + log.debug('Size :', data.length) + return null + } + + const salt = data.slice(0, SALT_byte_length) + const box = SecretBox.fromBytes(data.slice(SALT_byte_length)) + if (box === null) return null + + return new this(box, salt) + } } diff --git a/src/secret-box.ts b/src/secret-box.ts index c829ed9..0ccfc75 100644 --- a/src/secret-box.ts +++ b/src/secret-box.ts @@ -4,6 +4,8 @@ import {BIT_LENGTH, ENCRYPTION_ALGORITHM} from './const' const log = logger('crypto:secret-box') +const IV_byte_length = 12 + export default class SecretBox { private constructor( private readonly iv: Uint8Array, @@ -25,7 +27,7 @@ export default class SecretBox { public static async encrypt(data: Uint8Array, key: CryptoKey): Promise { log.trace('encrypt') - const iv = crypto.getRandomValues(new Uint8Array(12)) + const iv = crypto.getRandomValues(new Uint8Array(IV_byte_length)) let cipher: ArrayBuffer try { @@ -43,7 +45,7 @@ export default class SecretBox { return null } - return new SecretBox(iv, new Uint8Array(cipher)) + return new this(iv, new Uint8Array(cipher)) } public async decrypt(key: CryptoKey): Promise { log.trace('decrypt') @@ -86,6 +88,22 @@ export default class SecretBox { const cipher = b642a(parts[1]) if (cipher === null) return null - return new SecretBox(iv, cipher) + return new this(iv, cipher) + } + + public toBytes(): Uint8Array { + return new Uint8Array([...this.iv, ...this.cipher]) + } + public static fromBytes(data: Uint8Array): SecretBox | null { + if (data.length <= IV_byte_length) { + log.warn(`Data is too short (minimum size : more than ${IV_byte_length} bytes)`) + log.debug('Actual size :', data.length) + return null + } + + const iv = data.slice(0, IV_byte_length) + const cipher = data.slice(IV_byte_length) + + return new this(iv, cipher) } } diff --git a/test/misc.test.ts b/test/misc.test.ts index 6dbde25..d165d09 100644 --- a/test/misc.test.ts +++ b/test/misc.test.ts @@ -1,6 +1,6 @@ import {expect, test} from 'bun:test' import {PrivateBox, PrivateWrap, SecretBox, signature} from '..' -import {pubkey_fromString, pubkey_toString, Usage} from '../src/misc' +import {pubkey_fromBytes, pubkey_fromString, pubkey_toBytes, pubkey_toString, Usage} from '../src/misc' test('Private box', async () => { const message = crypto.getRandomValues(new Uint8Array(8)) @@ -48,3 +48,16 @@ test('Signature', async () => { const verification = await signature.verify(message, de!, signed!) expect(verification).toBeTrue() }) +test('Byte serialization', async () => { + const k = await signature.gen(false) + const message = crypto.getRandomValues(new Uint8Array(8)) + const signed = await signature.sign(message, k.privateKey) + expect(signed).not.toBeNull() + + const ser = await pubkey_toBytes(k.publicKey) + const de = await pubkey_fromBytes(ser, Usage.sign) + expect(de).not.toBeNull() + + const verification = await signature.verify(message, de!, signed!) + expect(verification).toBeTrue() +}) diff --git a/test/private-box.test.ts b/test/private-box.test.ts index 77bde6c..c35d503 100644 --- a/test/private-box.test.ts +++ b/test/private-box.test.ts @@ -52,7 +52,7 @@ test('Encrypt with different context', async () => { expect(unboxed).toBeNull() }) -test('serialization', async () => { +test('String serialization', async () => { const box = await PrivateBox.encrypt(message, k1.publicKey) expect(box).not.toBeNull() @@ -66,3 +66,17 @@ test('serialization', async () => { expect(unboxed).not.toBeNull() expect(unboxed).toEqual(message) }) +test('Byte serialization', async () => { + const box = await PrivateBox.encrypt(message, k1.publicKey) + expect(box).not.toBeNull() + + const ser = await box!.toBytes() + const de = await PrivateBox.fromBytes(ser) + + expect(de).not.toBeNull() + expect(de).toEqual(box) + + const unboxed = await de!.decrypt(k1.privateKey) + expect(unboxed).not.toBeNull() + expect(unboxed).toEqual(message) +}) diff --git a/test/pwd-box.test.ts b/test/pwd-box.test.ts index e641f4f..6a9e911 100644 --- a/test/pwd-box.test.ts +++ b/test/pwd-box.test.ts @@ -39,7 +39,7 @@ test("Encryption doesn't work with different context", async () => { expect(unboxed).toBeNull() }) -test('serialization', async () => { +test('String serialization', async () => { const box = await PwdBox.encrypt(message, k1) expect(box).not.toBeNull() @@ -53,3 +53,17 @@ test('serialization', async () => { expect(unboxed).not.toBeNull() expect(unboxed).toEqual(message) }) +test('Byte serialization', async () => { + const box = await PwdBox.encrypt(message, k1) + expect(box).not.toBeNull() + + const ser = box!.toBytes() + const de = PwdBox.fromBytes(ser) + + expect(de).not.toBeNull() + expect(de).toEqual(box) + + const unboxed = await de!.decrypt(k1) + expect(unboxed).not.toBeNull() + expect(unboxed).toEqual(message) +})