From 81ec3e9ed0c59fb3b84143170bfdaf77b91fb962 Mon Sep 17 00:00:00 2001 From: Pascal Perrenoud Date: Sat, 27 Jul 2024 13:35:17 +0200 Subject: [PATCH] fmt --- src/jwt.ts | 96 +++++++++++++------------ src/kdf.ts | 150 +++++++++++++++++++--------------------- src/private-wrap.ts | 81 +++++++++------------- src/secret-box.ts | 103 ++++++++++++++------------- src/secret-wrap.ts | 140 ++++++++++++++++++------------------- src/signature.derive.ts | 122 ++++++++++++++++---------------- src/signature.ts | 62 ++++++++--------- 7 files changed, 364 insertions(+), 390 deletions(-) diff --git a/src/jwt.ts b/src/jwt.ts index 454f7c9..ac5ada4 100644 --- a/src/jwt.ts +++ b/src/jwt.ts @@ -6,53 +6,57 @@ const log = logger('crypto:jwt') export type Key = jose.KeyLike | Uint8Array export class JWTcontext { - private constructor( - private readonly key: Key, - ) {} + private constructor(private readonly key: Key) {} - public static async gen_key(): Promise { - log.trace("generate key") - return jose.generateSecret("HS512") - } - public static new(key: Key) : JWTcontext { - return new JWTcontext(key) - } - public static async new_random() : Promise { - const k = await JWTcontext.gen_key() - return new JWTcontext(k) + public static async gen_key(): Promise { + log.trace('generate key') + return jose.generateSecret('HS512') + } + public static new(key: Key): JWTcontext { + return new JWTcontext(key) + } + public static async new_random(): Promise { + const k = await JWTcontext.gen_key() + return new JWTcontext(k) + } + + public async sign( + message: T, + set_issued: boolean = false, + exp?: number | string | Date, + audience?: string | string[], + issuer?: string + ): Promise { + log.trace('sign JWT') + log.trace('Config :', { + set_issued, + exp, + issuer + }) + + let jwt = new jose.SignJWT({message}).setProtectedHeader({alg: 'HS512'}) + + if (set_issued) jwt = jwt.setIssuedAt() + if (issuer !== undefined) jwt = jwt.setIssuer(issuer) + if (audience !== undefined) jwt = jwt.setAudience(audience) + if (exp !== undefined) jwt = jwt.setExpirationTime(exp) + + return await jwt.sign(this.key) + } + + public async verify(jwt: string, audience?: string | string[], issuer?: string | string[]): Promise { + log.debug('Verify JWT') + log.trace('Issuer :', issuer) + log.trace('Audience :', audience) + + try { + let payload = await jose.jwtVerify(jwt, this.key, {audience, issuer}) + return payload.payload.message as T + } catch (e) { + log.warn('JWT verification failed') + log.debug(`Error : ${e}`) } - public async sign(message: T, set_issued: boolean = false, exp?: number | string | Date, audience?: string | string[], issuer?: string): Promise { - log.trace('sign JWT') - log.trace('Config :', { - set_issued, - exp, - issuer, - }) - - let jwt = new jose.SignJWT({message}).setProtectedHeader({alg: "HS512"}) - - if (set_issued) jwt = jwt.setIssuedAt() - if (issuer !== undefined) jwt = jwt.setIssuer(issuer) - if (audience !== undefined) jwt = jwt.setAudience(audience) - if (exp !== undefined) jwt = jwt.setExpirationTime(exp) - - return await jwt.sign(this.key) - } - - public async verify(jwt: string, audience?: string | string[], issuer?: string | string[]): Promise { - log.debug('Verify JWT') - log.trace('Issuer :', issuer) - log.trace('Audience :', audience) - - try { - let payload = await jose.jwtVerify(jwt, this.key, {audience, issuer}) - return payload.payload.message as T - } catch (e) { - log.warn('JWT verification failed') - log.debug(`Error : ${e}`) - } - - return null - } + return null + } } diff --git a/src/kdf.ts b/src/kdf.ts index 929bacc..8d1358f 100644 --- a/src/kdf.ts +++ b/src/kdf.ts @@ -2,89 +2,79 @@ import logger from 'log' const log = logger('crypto:kdf') export enum Usage { - sign, - wrap, + sign, + wrap } -export async function hkdf(key: Uint8Array, usage: Usage) : Promise { - log.trace("HKDF") - log.trace(`usage : ${usage === Usage.sign ? 'sign' : 'wrap'}`) +export async function hkdf(key: Uint8Array, usage: Usage): Promise { + log.trace('HKDF') + log.trace(`usage : ${usage === Usage.sign ? 'sign' : 'wrap'}`) - const material = await crypto.subtle.importKey( - "raw", - key, - "HKDF", - false, - ["deriveKey", "deriveBits"], - ) + const material = await crypto.subtle.importKey('raw', key, 'HKDF', false, ['deriveKey', 'deriveBits']) - if (usage === Usage.wrap) { - return crypto.subtle.deriveKey( - { - name: "HKDF", - hash: "SHA-512", - salt: new Uint8Array(32), - info: new TextEncoder().encode("wrap"), - }, - material, - {name: "AES-GCM", length: 256}, - false, - ["wrapKey", "unwrapKey"], - ) - } else if (usage === Usage.sign) { - const buffer = await crypto.subtle.deriveBits( - { - name: "HKDF", - hash: "SHA-512", - salt: new Uint8Array(32), - info: new TextEncoder().encode("sign"), - }, - material, - 512 - ) - return new Uint8Array(buffer) - } else { - log.warn(`Called HKDF with unknown enum value : ${usage}`) - throw "I don't even know what to say." - } -} - -export async function pbkdf(salt: Uint8Array, password: string) : Promise { - log.trace("PBKDF") - const material = await crypto.subtle.importKey( - "raw", - new TextEncoder().encode(password), - "PBKDF2", - false, - ["deriveBits"], - ) - - const buffer = await crypto.subtle.deriveBits( - { - name: "PBKDF2", - salt, - iterations: 250_000, - hash: "SHA-512", - }, - material, - 256, - ) - - return new Uint8Array(buffer) -} - -export function ecdh(privkey: CryptoKey, pubkey: CryptoKey) : Promise { - log.trace("ECDH") + if (usage === Usage.wrap) { return crypto.subtle.deriveKey( - { - name: "ECDH", - public: pubkey, - }, - privkey, - { - name: "AES-GCM", - length: 256, - }, - false, - ["wrapKey", "unwrapKey"], + { + name: 'HKDF', + hash: 'SHA-512', + salt: new Uint8Array(32), + info: new TextEncoder().encode('wrap') + }, + material, + {name: 'AES-GCM', length: 256}, + false, + ['wrapKey', 'unwrapKey'] ) + } else if (usage === Usage.sign) { + const buffer = await crypto.subtle.deriveBits( + { + name: 'HKDF', + hash: 'SHA-512', + salt: new Uint8Array(32), + info: new TextEncoder().encode('sign') + }, + material, + 512 + ) + return new Uint8Array(buffer) + } else { + log.warn(`Called HKDF with unknown enum value : ${usage}`) + throw "I don't even know what to say." + } +} + +export async function pbkdf(salt: Uint8Array, password: string): Promise { + log.trace('PBKDF') + const material = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, [ + 'deriveBits' + ]) + + const buffer = await crypto.subtle.deriveBits( + { + name: 'PBKDF2', + salt, + iterations: 250_000, + hash: 'SHA-512' + }, + material, + 256 + ) + + return new Uint8Array(buffer) +} + +export function ecdh(privkey: CryptoKey, pubkey: CryptoKey): Promise { + log.trace('ECDH') + return crypto.subtle.deriveKey( + { + name: 'ECDH', + public: pubkey + }, + privkey, + { + name: 'AES-GCM', + length: 256 + }, + false, + ['wrapKey', 'unwrapKey'] + ) } diff --git a/src/private-wrap.ts b/src/private-wrap.ts index 90ea112..ae9925e 100644 --- a/src/private-wrap.ts +++ b/src/private-wrap.ts @@ -7,57 +7,44 @@ import {ecdh} from './kdf' const log = logger('crypto:private-wrap') const algorithm = { - name: "ECDH", - namedCurve: "P-521", + name: 'ECDH', + namedCurve: 'P-521' } export class PrivateWrap { - private constructor( - private readonly box: SecretWrap, - private readonly pubkey: CryptoKey, - ) {} + private constructor(private readonly box: SecretWrap, private readonly pubkey: CryptoKey) {} - public static gen_keypair(extractable : boolean = true) : Promise { - log.trace("generate keypair") - return crypto.subtle.generateKey( - algorithm, - extractable, - ["deriveKey"], - ) - } + public static gen_keypair(extractable: boolean = true): Promise { + log.trace('generate keypair') + return crypto.subtle.generateKey(algorithm, extractable, ['deriveKey']) + } - public static async encrypt(data: CryptoKey, pubkey: CryptoKey) : Promise { - log.trace("encrypt") - const k = await this.gen_keypair() - const kd = await ecdh(k.privateKey, pubkey) - const box = await SecretWrap.encrypt(data, kd) - return new this(box, k.publicKey) - } - public async decrypt(privkey: CryptoKey) : Promise { - log.trace("decrypt") - const kd = await ecdh(privkey, this.pubkey) - return this.box.decrypt(kd) - } + public static async encrypt(data: CryptoKey, pubkey: CryptoKey): Promise { + log.trace('encrypt') + const k = await this.gen_keypair() + const kd = await ecdh(k.privateKey, pubkey) + const box = await SecretWrap.encrypt(data, kd) + return new this(box, k.publicKey) + } + public async decrypt(privkey: CryptoKey): Promise { + log.trace('decrypt') + const kd = await ecdh(privkey, this.pubkey) + return this.box.decrypt(kd) + } - public async toString(): Promise { - log.trace("toString") - const pubkey_spki = await crypto.subtle.exportKey("spki", this.pubkey) - const pubkey = a2b64(new Uint8Array(pubkey_spki)) - const box = this.box.toString() - return `${pubkey}.${box}` - } - public static async fromString(data: string): Promise { - log.trace("fromString") - const parts = data.split(".", 2) - const pubkey_str = b642a(parts[0]).expect("Failed to decode pubkey") - const pubkey = await crypto.subtle.importKey( - "spki", - pubkey_str, - algorithm, - true, - [], - ) - const box = SecretWrap.fromString(parts[1]) - return new PrivateWrap(box, pubkey) - } + public async toString(): Promise { + log.trace('toString') + const pubkey_spki = await crypto.subtle.exportKey('spki', this.pubkey) + const pubkey = a2b64(new Uint8Array(pubkey_spki)) + const box = this.box.toString() + return `${pubkey}.${box}` + } + public static async fromString(data: string): Promise { + log.trace('fromString') + const parts = data.split('.', 2) + const pubkey_str = b642a(parts[0]).expect('Failed to decode pubkey') + const pubkey = await crypto.subtle.importKey('spki', pubkey_str, algorithm, true, []) + const box = SecretWrap.fromString(parts[1]) + return new PrivateWrap(box, pubkey) + } } diff --git a/src/secret-box.ts b/src/secret-box.ts index 1d3b53d..36900e7 100644 --- a/src/secret-box.ts +++ b/src/secret-box.ts @@ -4,60 +4,57 @@ import {a2b64, b642a} from 'misc' const log = logger('crypto:secret-box') export class SecretBox { - private constructor( - private readonly iv: Uint8Array, - private readonly cipher: Uint8Array, - ) {} + private constructor(private readonly iv: Uint8Array, private readonly cipher: Uint8Array) {} - public static gen_key(extractable : boolean = true) : Promise { - log.trace("generate key") - return crypto.subtle.generateKey( - { - name: "AES-GCM", - length: 256, - }, - extractable, - ["encrypt", "decrypt"], - ) - } + public static gen_key(extractable: boolean = true): Promise { + log.trace('generate key') + return crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: 256 + }, + extractable, + ['encrypt', 'decrypt'] + ) + } - public static async encrypt(data: Uint8Array, key: CryptoKey) : Promise { - log.trace("encrypt") - const iv = crypto.getRandomValues(new Uint8Array(11)) - const cipher = await crypto.subtle.encrypt( - { - name: "AES-GCM", - iv, - }, - key, - data - ) - return new SecretBox(iv, new Uint8Array(cipher)) - } - public async decrypt(key: CryptoKey) : Promise { - log.trace("decrypt") - const buffer = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv: this.iv - }, - key, - this.cipher - ) - return new Uint8Array(buffer) - } + public static async encrypt(data: Uint8Array, key: CryptoKey): Promise { + log.trace('encrypt') + const iv = crypto.getRandomValues(new Uint8Array(11)) + const cipher = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv + }, + key, + data + ) + return new SecretBox(iv, new Uint8Array(cipher)) + } + public async decrypt(key: CryptoKey): Promise { + log.trace('decrypt') + const buffer = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: this.iv + }, + key, + this.cipher + ) + return new Uint8Array(buffer) + } - public toString() : string { - log.trace("toString") - const iv = a2b64(this.iv) - const cipher = a2b64(this.iv) - return `${iv}.${cipher}` - } - public static fromString(data: string) : SecretBox { - log.trace("fromString") - const parts = data.split(".", 2) - const iv = b642a(parts[0]).expect("Failed decode IV") - const cipher = b642a(parts[1]).expect("Failed to decode cipher") - return new SecretBox(iv, cipher) - } + public toString(): string { + log.trace('toString') + const iv = a2b64(this.iv) + const cipher = a2b64(this.iv) + return `${iv}.${cipher}` + } + public static fromString(data: string): SecretBox { + log.trace('fromString') + const parts = data.split('.', 2) + const iv = b642a(parts[0]).expect('Failed decode IV') + const cipher = b642a(parts[1]).expect('Failed to decode cipher') + return new SecretBox(iv, cipher) + } } diff --git a/src/secret-wrap.ts b/src/secret-wrap.ts index 0004bf9..ca3f423 100644 --- a/src/secret-wrap.ts +++ b/src/secret-wrap.ts @@ -4,80 +4,78 @@ import {a2b64, b642a} from 'misc' const log = logger('crypto:secret-wrap') export class SecretWrap { - constructor( - private readonly cipher: Uint8Array, - private readonly algorithm: KeyAlgorithm, - private readonly usages: KeyUsage[], - private readonly type: "raw" | "pkcs8", - private readonly iv: Uint8Array, - ) {} + constructor( + private readonly cipher: Uint8Array, + private readonly algorithm: KeyAlgorithm, + private readonly usages: KeyUsage[], + private readonly type: 'raw' | 'pkcs8', + private readonly iv: Uint8Array + ) {} - public static gen_key(extractable : boolean = true) : Promise { - log.trace("generate key") - return crypto.subtle.generateKey( - { - name: "AES-GCM", - length: 256, - }, - extractable, - ["wrapKey", "unwrapKey"], - ) - } + public static gen_key(extractable: boolean = true): Promise { + log.trace('generate key') + return crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: 256 + }, + extractable, + ['wrapKey', 'unwrapKey'] + ) + } - private static format(type: KeyType) : "raw" | "pkcs8" { - switch (type) { - case "private": return "pkcs8" - case "secret": return "raw" - default: throw "Don't wrap public keys please..." - } + private static format(type: KeyType): 'raw' | 'pkcs8' { + switch (type) { + case 'private': + return 'pkcs8' + case 'secret': + return 'raw' + default: + throw "Don't wrap public keys please..." } + } - public static async encrypt(data: CryptoKey, key: CryptoKey) : Promise { - log.trace("encrypt") - const format = this.format(data.type) - const iv = crypto.getRandomValues(new Uint8Array(12)) - const box = await crypto.subtle.wrapKey( - format, - data, - key, - { - name: "AES-GCM", - iv, - }, - ) - return new SecretWrap(new Uint8Array(box), data.algorithm, data.usages, format, iv) - } - public async decrypt(key: CryptoKey) : Promise { - log.trace("decrypt") - return crypto.subtle.unwrapKey( - this.type, - this.cipher, - key, - { - name: "AES-GCM", - iv: this.iv, - }, - this.algorithm, - true, - this.usages - ) - } + public static async encrypt(data: CryptoKey, key: CryptoKey): Promise { + log.trace('encrypt') + const format = this.format(data.type) + const iv = crypto.getRandomValues(new Uint8Array(12)) + const box = await crypto.subtle.wrapKey(format, data, key, { + name: 'AES-GCM', + iv + }) + return new SecretWrap(new Uint8Array(box), data.algorithm, data.usages, format, iv) + } + public async decrypt(key: CryptoKey): Promise { + log.trace('decrypt') + return crypto.subtle.unwrapKey( + this.type, + this.cipher, + key, + { + name: 'AES-GCM', + iv: this.iv + }, + this.algorithm, + true, + this.usages + ) + } - public toString() : string { - log.trace("toString") - return JSON.stringify({ - cipher: a2b64(this.cipher), - iv: a2b64(this.iv), - algorithm: this.algorithm, - usages: this.usages, - type: this.type, - }) - } - public static fromString(data: string) : SecretWrap { - log.trace("fromString") - const obj = JSON.parse(data) - const cipher = b642a(obj.cipher).expect("Failed to decode cipher") - const iv = b642a(obj.iv).expect("Failed to decode IV") - return new SecretWrap(cipher, obj.algorithm, obj.usages, obj.type, iv) - } + public toString(): string { + log.trace('toString') + return JSON.stringify({ + cipher: a2b64(this.cipher), + iv: a2b64(this.iv), + algorithm: this.algorithm, + usages: this.usages, + type: this.type + }) + } + public static fromString(data: string): SecretWrap { + log.trace('fromString') + const obj = JSON.parse(data) + const cipher = b642a(obj.cipher).expect('Failed to decode cipher') + const iv = b642a(obj.iv).expect('Failed to decode IV') + return new SecretWrap(cipher, obj.algorithm, obj.usages, obj.type, iv) + } } diff --git a/src/signature.derive.ts b/src/signature.derive.ts index a5f01c7..eba76a5 100644 --- a/src/signature.derive.ts +++ b/src/signature.derive.ts @@ -9,74 +9,72 @@ import type {ProjPointType} from '@noble/curves/abstract/weierstrass' * @param source entropy source, 64 bytes * @returns [PrivKey, PubKey] */ -export async function derive_keypair(source: Uint8Array) : Promise<[CryptoKey, CryptoKey]> { - const derived = hkdf(sha512, source, undefined, 'sign', 512) - const validPrivateKey = mod.mapHashToField(derived, p521.CURVE.n) // Dodge modulus bias :) (kinda) - const d = a2bg(validPrivateKey) - const point = get_pubkey(d) +export async function derive_keypair(source: Uint8Array): Promise<[CryptoKey, CryptoKey]> { + const derived = hkdf(sha512, source, undefined, 'sign', 512) + const validPrivateKey = mod.mapHashToField(derived, p521.CURVE.n) // Dodge modulus bias :) (kinda) + const d = a2bg(validPrivateKey) + const point = get_pubkey(d) - const pubkey = await crypto_pubkey(point) - const privkey = await crypto_privkey(d, point) + const pubkey = await crypto_pubkey(point) + const privkey = await crypto_privkey(d, point) - return [privkey, pubkey] + return [privkey, pubkey] } -function get_pubkey(d: bigint) : ProjPointType { - return p521.ProjectivePoint.BASE.multiply(d) +function get_pubkey(d: bigint): ProjPointType { + return p521.ProjectivePoint.BASE.multiply(d) } -function crypto_privkey(d: bigint, point: ProjPointType) : Promise { - const jwk = { - crv: "P-521", - d: bg2b64url(d), - ext: false, - key_ops: [ "sign" ], - kty: "EC", - x: bg2b64url(point.x), - y: bg2b64url(point.y), - } - return crypto.subtle.importKey( - "jwk", - jwk, - { - name: "ECDSA", - namedCurve: "P-521" - }, - false, - ["sign"] - ) +function crypto_privkey(d: bigint, point: ProjPointType): Promise { + const jwk = { + crv: 'P-521', + d: bg2b64url(d), + ext: false, + key_ops: ['sign'], + kty: 'EC', + x: bg2b64url(point.x), + y: bg2b64url(point.y) + } + return crypto.subtle.importKey( + 'jwk', + jwk, + { + name: 'ECDSA', + namedCurve: 'P-521' + }, + false, + ['sign'] + ) } -function crypto_pubkey(point: ProjPointType) : Promise { - const jwk = { - crv: "P-521", - ext: true, - key_ops: [ "verify" ], - kty: "EC", - x: bg2b64url(point.x), - y: bg2b64url(point.y), - } - return crypto.subtle.importKey( - "jwk", - jwk, - { - name: "ECDSA", - namedCurve: "P-521" - }, - true, - ["verify"] - ) +function crypto_pubkey(point: ProjPointType): Promise { + const jwk = { + crv: 'P-521', + ext: true, + key_ops: ['verify'], + kty: 'EC', + x: bg2b64url(point.x), + y: bg2b64url(point.y) + } + return crypto.subtle.importKey( + 'jwk', + jwk, + { + name: 'ECDSA', + namedCurve: 'P-521' + }, + true, + ['verify'] + ) } -function bg2b64url(n: bigint) : string { - return encode(btoa(String.fromCharCode(...hexToBytes(numberToHexUnpadded(n))))) +function bg2b64url(n: bigint): string { + return encode(btoa(String.fromCharCode(...hexToBytes(numberToHexUnpadded(n))))) } -function encode(input: string) : string { - // Replace non-url compatible chars with base64 standard chars - input = input - .replace(/\+/g, '-') - .replace(/\//g, '_') +function encode(input: string): string { + // Replace non-url compatible chars with base64 standard chars + input = input.replace(/\+/g, '-').replace(/\//g, '_') - const last = input.lastIndexOf("=") - if (last > 0) return input.substring(0, last) - return input + const last = input.lastIndexOf('=') + if (last > 0) return input.substring(0, last) + return input +} +function a2bg(data: Uint8Array): bigint { + return hexToNumber(bytesToHex(data)) } -function a2bg(data: Uint8Array) : bigint { - return hexToNumber(bytesToHex(data)) -} \ No newline at end of file diff --git a/src/signature.ts b/src/signature.ts index 845d758..4325384 100644 --- a/src/signature.ts +++ b/src/signature.ts @@ -1,38 +1,38 @@ import logger from 'log' const log = logger('crypto:signature') -export async function gen_keypair(extractable : boolean = true) : Promise { - return crypto.subtle.generateKey( - { - name: "ECDSA", - namedCurve: "P-521", - }, - extractable, - ["sign", "verify"] - ) +export async function gen_keypair(extractable: boolean = true): Promise { + return crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-521' + }, + extractable, + ['sign', 'verify'] + ) } export {derive_keypair} from './signature.derive' -export async function sign(message: Uint8Array, privkey: CryptoKey) : Promise { - log.trace("sign") - const buffer = await crypto.subtle.sign( - { - name: "ECDSA", - hash: "SHA-512" - }, - privkey, - message - ) - return new Uint8Array(buffer) +export async function sign(message: Uint8Array, privkey: CryptoKey): Promise { + log.trace('sign') + const buffer = await crypto.subtle.sign( + { + name: 'ECDSA', + hash: 'SHA-512' + }, + privkey, + message + ) + return new Uint8Array(buffer) } -export async function verify(message: Uint8Array, pubkey: CryptoKey, signature: Uint8Array) : Promise { - log.trace("verify") - return crypto.subtle.verify( - { - name: "ECDSA", - hash: "SHA-512" - }, - pubkey, - signature, - message, - ) +export async function verify(message: Uint8Array, pubkey: CryptoKey, signature: Uint8Array): Promise { + log.trace('verify') + return crypto.subtle.verify( + { + name: 'ECDSA', + hash: 'SHA-512' + }, + pubkey, + signature, + message + ) }