From 61f77fab0ca9c8dfb6edb78fd98397049bf88f8e Mon Sep 17 00:00:00 2001 From: Pascal Perrenoud Date: Wed, 15 May 2024 12:44:00 +0200 Subject: [PATCH] Logging Closes #11 --- package.json | 1 + src/boxes/asymmetric.ts | 26 +++++++++++++++++++------ src/boxes/private-wrap.ts | 41 ++++++++++++++++++++++++++------------- src/boxes/pwd-wrap.ts | 7 +++++++ src/boxes/pwd.ts | 28 +++++++++++++++----------- src/boxes/secret-wrap.ts | 39 +++++++++++++++++++++++++++++-------- src/boxes/symmetric.ts | 26 ++++++++++++++++++++++--- src/jwt.ts | 22 ++++++++++++++++++++- src/pbkdf.ts | 7 +++++++ src/signature.ts | 19 ++++++++++++++++-- 10 files changed, 171 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index aaa8ff1..3b7c89e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "jose": "^5.3.0", "result": "git+git@git.pband.ch:typescript/result", "misc": "git+git@git.pband.ch:typescript/misc", + "log": "git+git@git.pband.ch:typescript/log", "zxcvbn": "^4.4.2" }, "devDependencies": { diff --git a/src/boxes/asymmetric.ts b/src/boxes/asymmetric.ts index 8a7ec0d..998a7df 100644 --- a/src/boxes/asymmetric.ts +++ b/src/boxes/asymmetric.ts @@ -1,21 +1,25 @@ import {Result} from 'result' 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 cipher: Uint8Array; readonly _phantom!: T; - private constructor(cipher: Uint8Array) { - this.cipher = cipher - } + 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", @@ -31,18 +35,28 @@ export class PrivateBox { } 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(_) {} + } 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(_) {} + } catch(e) { + log.warn('decryption failed') + log.debug(`error : ${e}`) + } return Result.error([]) } diff --git a/src/boxes/private-wrap.ts b/src/boxes/private-wrap.ts index fe67e5b..7e982af 100644 --- a/src/boxes/private-wrap.ts +++ b/src/boxes/private-wrap.ts @@ -1,24 +1,21 @@ import {Result} from 'result' 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 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 - } + 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 @@ -26,6 +23,9 @@ export class PrivateWrap { * @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", @@ -49,6 +49,8 @@ export class PrivateWrap { * @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 { @@ -60,7 +62,10 @@ export class PrivateWrap { ) return Result.ok(new PrivateWrap(new Uint8Array(wrapped), key.algorithm, key.usages, format)) - } catch (_) {} + } catch (e) { + log.warn('wrap failed') + log.debug(`error : ${e}`) + } return Result.error([]) } @@ -70,6 +75,8 @@ export class PrivateWrap { * @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, @@ -81,7 +88,10 @@ export class PrivateWrap { this.usage ) return Result.ok(key) - } catch(_) {} + } catch(e) { + log.warn('unwrap failed') + log.debug(`error : ${e}`) + } return Result.error([]) } @@ -103,7 +113,10 @@ export class PrivateWrap { if (key.error()) return Result.error([]) return Result.ok(new PrivateWrap(key.unwrap(), algorithm, usage, format)) - } catch (_) {} + } 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 index 61dc449..af26068 100644 --- a/src/boxes/pwd-wrap.ts +++ b/src/boxes/pwd-wrap.ts @@ -2,6 +2,9 @@ import {Result} from 'result' 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( @@ -10,6 +13,8 @@ export class PwdWrap { ) {} 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) @@ -20,6 +25,8 @@ export class PwdWrap { } 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) diff --git a/src/boxes/pwd.ts b/src/boxes/pwd.ts index 9fef911..c488b03 100644 --- a/src/boxes/pwd.ts +++ b/src/boxes/pwd.ts @@ -2,30 +2,34 @@ import {Result} from 'result' 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 readonly secret_box: SecretBox - private readonly salt: Uint8Array - private constructor(secret_box: SecretBox, salt: Uint8Array) { - this.secret_box = secret_box - this.salt = salt - } + 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.gen_key(pwd, salt) + 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> { - const key = await PwdBox.gen_key(pwd, this.salt) + log.trace('decrypt') + const key = await PwdBox.get_key(pwd, this.salt) return this.secret_box.decrypt(key) } - private static async gen_key(pwd: string, salt: Uint8Array) : Promise { + private static async get_key(pwd: string, salt: Uint8Array) : Promise { return pbkdf(pwd, salt, ["encrypt", "decrypt"]) } @@ -35,10 +39,12 @@ export class PwdBox { return `${salt}${box}` } public static fromString(data: string) : Result> { - const salt = misc.b642a(data.slice(0, 24)) + const salt64 = data.slice(0, 24) + const salt = misc.b642a(salt64) if (salt.error()) return Result.error([]) - const box = SecretBox.fromString(data.slice(24)) + const box64 = data.slice(24) + const box = SecretBox.fromString(box64) if (box.error()) 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 index 9937e6b..641a87c 100644 --- a/src/boxes/secret-wrap.ts +++ b/src/boxes/secret-wrap.ts @@ -1,8 +1,11 @@ import {Result} from 'result' 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, @@ -12,6 +15,9 @@ export class SecretWrap { 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, @@ -20,6 +26,8 @@ export class SecretWrap { } 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)) @@ -35,12 +43,17 @@ export class SecretWrap { ) return Result.ok(new SecretWrap(new Uint8Array(wrapped_key), key_to_wrap.algorithm, key_to_wrap.usages, format, iv)) - } catch (_) {} + } 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, @@ -56,7 +69,10 @@ export class SecretWrap { ) return Result.ok(key) - } catch (_) {} + } catch (e) { + log.warn('Unwrapping failed') + log.debug(`Error : ${e}`) + } return Result.error([]) } @@ -73,13 +89,20 @@ export class SecretWrap { }) } 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) + 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.error()) return Result.error([]) - const wrapped_key = misc.b642a(wrapped_key64) - if (wrapped_key.error()) return Result.error([]) + 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())) + 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 index 9fa5ef0..701658c 100644 --- a/src/boxes/symmetric.ts +++ b/src/boxes/symmetric.ts @@ -1,8 +1,11 @@ import {Result} from 'result' import * as misc from 'misc' +import logger from 'log' export type Key = CryptoKey +const log = logger("crypto:boxes:secret") + export class SecretBox { private readonly cipher: Uint8Array private readonly iv: Uint8Array @@ -14,6 +17,9 @@ export class SecretBox { } 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", @@ -25,24 +31,34 @@ export class SecretBox { } 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 (_) {} + } 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 (_) {} + } catch (e) { + log.warn('Decryption failed') + log.debug(`Error : ${e}`) + } return Result.error([]) } @@ -54,7 +70,11 @@ export class SecretBox { } public static fromString(data: string) : Result> { const parts = data.split(".") - if (parts.length !== 2) return Result.error([]) + 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]) diff --git a/src/jwt.ts b/src/jwt.ts index ff27015..9ae3610 100644 --- a/src/jwt.ts +++ b/src/jwt.ts @@ -1,5 +1,8 @@ import {Result} from 'result' import * as jose from 'jose' +import logger from 'log' + +const log = logger('crypto:jwt') export type JWTstring = `${string}.${string}.${string}` export type JWTalgorithm = "HS256" | "HS512" | "ES256" | "ES512" | "EdDSA" @@ -14,6 +17,9 @@ export class JWTcontext { ) {} public static async gen_key(alg: JWTalgorithm, extractable: boolean = false) : Promise { + log.trace(`Generate new ${alg} key`) + log.debug('Key extractable :', extractable ? 'yes' : 'no') + switch (alg) { case "HS256": case "HS512": @@ -29,6 +35,13 @@ export class JWTcontext { } public async sign(message: T, set_issued: boolean = false, exp?: number | string | Date, audience?: string | string[], issuer?: string): Promise> { + log.trace('sign JWT') + log.debug('Config :', { + set_issued, + exp, + issuer, + }) + let jwt = new jose.SignJWT({payload: message}).setProtectedHeader({ alg: this.alg }) if (set_issued) jwt = jwt.setIssuedAt() @@ -43,12 +56,18 @@ export class JWTcontext { } public async verify(jwt: JWT, audience?: string | string[], issuer?: string | string[]): Promise> { + log.trace('Verify JWT') + log.debug('Issuers :', issuer) + const key = this.get_key(false) try { let payload = await jose.jwtVerify(jwt.jwt, key) return Result.ok(payload.payload.payload as T) - } catch(_) {} + } catch(e) { + log.warn('JWT verification failed') + log.debug(`Error : ${e}`) + } return Result.error([]) } @@ -72,6 +91,7 @@ export class JWT { ) {} public get payload() : T { + log.trace("Decode payload of JWT") return jose.decodeJwt(this.jwt).payload as T } diff --git a/src/pbkdf.ts b/src/pbkdf.ts index 2fdfc53..c5cfbc8 100644 --- a/src/pbkdf.ts +++ b/src/pbkdf.ts @@ -1,4 +1,11 @@ +import logger from 'log' + +const log = logger('crypto:pbkdf') + export async function pbkdf(password: string, salt: Uint8Array, usages: KeyUsage[]): Promise { + log.trace('derive') + log.debug(`Usages : ${usages}`) + const keyMaterial = await window.crypto.subtle.importKey( "raw", new TextEncoder().encode(password), diff --git a/src/signature.ts b/src/signature.ts index e71e864..57657e7 100644 --- a/src/signature.ts +++ b/src/signature.ts @@ -1,4 +1,5 @@ import {Result} from 'result' +import logger from 'log' export type PrivKey = CryptoKey export type PubKey = CryptoKey @@ -8,6 +9,7 @@ const algorithm: EcdsaParams = { name: "ECDSA", hash: {name: "SHA-512"}, } +const log = logger('crypto:signature') /** * Create a new keypair for signing @@ -15,6 +17,9 @@ const algorithm: EcdsaParams = { * @return [privkey, pubkey] keys */ export async function gen_keypair(extractable: boolean = false) : Promise { + log.trace('Generating keypair') + log.debug('Extractable :', extractable ? 'yes' : 'no') + let key = await window.crypto.subtle.generateKey( { name: "ECDSA", @@ -27,18 +32,25 @@ export async function gen_keypair(extractable: boolean = false) : Promise> { + log.trace('sign') + try { return Result.ok(await window.crypto.subtle.sign( algorithm, privkey, message, )) - } catch(_) {} + } catch(e) { + log.warn('Signature failed') + log.debug(`Error : ${e}`) + } return Result.error([]) } export async function verify(pubkey: PubKey, message: Uint8Array, signature: ArrayBuffer): Promise { + log.trace('Verify signature') + try { return await window.crypto.subtle.verify( algorithm, @@ -46,7 +58,10 @@ export async function verify(pubkey: PubKey, message: Uint8Array, signature: Arr signature, message ); - } catch (_) {} + } catch (e) { + log.warn('Verification failed') + log.debug(`Error : ${e}`) + } return false; }