From 76cf4632e4deba52333f2e0dc912f2d4b380a8e7 Mon Sep 17 00:00:00 2001 From: Pascal Perrenoud Date: Thu, 12 Sep 2024 01:31:45 +0200 Subject: [PATCH] Implement strength for whole library --- index.ts | 13 +++++++++++++ src/const.ts | 20 ++++++++++++++++---- src/jwt.ts | 11 ++++++----- src/kdf.ts | 43 +++++++++++++++++++------------------------ src/misc.ts | 4 ++-- src/private-box.ts | 6 +++--- src/private-wrap.ts | 4 ++-- src/secret-box.ts | 10 +++++----- src/secret-wrap.ts | 10 +++++----- src/signature.ts | 17 +++++++++-------- test/init.ts | 5 ++++- test/kdf.test.ts | 14 +++++++++----- test/misc.test.ts | 3 ++- test/wrap.test.ts | 3 ++- 14 files changed, 97 insertions(+), 66 deletions(-) diff --git a/index.ts b/index.ts index 7628cd1..96ffc94 100644 --- a/index.ts +++ b/index.ts @@ -5,6 +5,19 @@ import PrivateWrap from './src/private-wrap' import PwdBox from './src/pwd-box' import PwdWrap from './src/pwd-wrap' +export enum Strength { + weak, + moderate, + strong +} + +export const set_strength = (new_strength: Strength) => { + if (!(new_strength in Strength)) throw 'Invalid strength !' + strength = new_strength +} +let strength = Strength.moderate +export let STRENGTH: () => Strength = () => strength + export * as kdf from './src/kdf' export * as misc from './src/misc' export * as signature from './src/signature' diff --git a/src/const.ts b/src/const.ts index 709b4a3..2cc0452 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,4 +1,16 @@ -export const ECDH = {name: 'ECDH', namedCurve: 'P-521'} -export const SIGNATURE_KEY = {name: 'ECDSA', namedCurve: 'P-521'} -export const SIGNATURE_ALGO = {name: 'ECDSA', hash: 'SHA-512'} -export const ENCRYPTION = 'AES-GCM' +import {STRENGTH} from '..' + +export const CURVE_NAME = () => ['P-256', 'P-384', 'P-521'][STRENGTH()] +export const HASH_NAME = () => ['SHA-256', 'SHA-384', 'SHA-512'][STRENGTH()] + +export const JWT_ALGORITHM = () => ['HS256', 'HS384', 'HS512'][STRENGTH()] + +export const ECDH_PARAMETERS = () => ({name: 'ECDH', namedCurve: CURVE_NAME()}) + +export const SIGNATURE_KEY = () => ({name: 'ECDSA', namedCurve: CURVE_NAME()}) +export const SIGNATURE_PARAMETERS = () => ({name: 'ECDSA', hash: HASH_NAME()}) + +export const ENCRYPTION_ALGORITHM = 'AES-GCM' + +export const BYTE_LENGTH = () => [16, 24, 32][STRENGTH()] +export const BIT_LENGTH = () => [128, 192, 256][STRENGTH()] diff --git a/src/jwt.ts b/src/jwt.ts index 967ef6f..70737af 100644 --- a/src/jwt.ts +++ b/src/jwt.ts @@ -1,16 +1,17 @@ -import * as jose from 'jose' import logger from 'log' +import {generateSecret, jwtVerify, type KeyLike, SignJWT} from 'jose' +import {JWT_ALGORITHM} from './const' const log = logger('crypto:jwt') -export type Key = jose.KeyLike | Uint8Array +export type Key = KeyLike | Uint8Array export class JWTcontext { private constructor(private readonly key: Key) {} public static async gen_key(): Promise { log.trace('generate key') - return await jose.generateSecret('HS512') + return await generateSecret(JWT_ALGORITHM()) } public static new(key: Key): JWTcontext { return new JWTcontext(key) @@ -34,7 +35,7 @@ export class JWTcontext { issuer }) - let jwt = new jose.SignJWT({message}).setProtectedHeader({alg: 'HS512'}) + let jwt = new SignJWT({message}).setProtectedHeader({alg: JWT_ALGORITHM()}) if (set_issued) jwt = jwt.setIssuedAt() if (issuer !== undefined) jwt = jwt.setIssuer(issuer) @@ -50,7 +51,7 @@ export class JWTcontext { log.trace('Audience :', audience) try { - let payload = await jose.jwtVerify(jwt, this.key, {audience, issuer}) + let payload = await jwtVerify(jwt, this.key, {audience, issuer}) return payload.payload.message as T } catch (e) { log.warn('JWT verification failed') diff --git a/src/kdf.ts b/src/kdf.ts index 10b9440..b78cc6b 100644 --- a/src/kdf.ts +++ b/src/kdf.ts @@ -1,5 +1,7 @@ import logger from 'log' -import * as consts from './const' + +import {STRENGTH, Strength} from '..' +import {BIT_LENGTH, BYTE_LENGTH, CURVE_NAME, ENCRYPTION_ALGORITHM, HASH_NAME} from './const' const log = logger('crypto:kdf') @@ -12,19 +14,13 @@ export enum DHusage { box, wrap } -export enum Strength { - weak, - moderate, - strong -} -/** Minimum seed size : 32 bytes */ export async function hkdf(key: Uint8Array, usage: Usage, context?: string): Promise { log.trace('HKDF') log.trace(`usage : ${usage}`) - if (key.length < 32) { - log.warn("Seed's size is less than 32") + if (key.length < BYTE_LENGTH()) { + log.warn("Seed's size is less than", BYTE_LENGTH()) return null } @@ -49,8 +45,8 @@ export async function hkdf(key: Uint8Array, usage: Usage, context?: string): Pro case Usage.box: { algo = { - name: consts.ENCRYPTION, - length: 256 + name: ENCRYPTION_ALGORITHM, + length: BIT_LENGTH() } usages = ['encrypt', 'decrypt'] info_txt = 'box' @@ -59,8 +55,8 @@ export async function hkdf(key: Uint8Array, usage: Usage, context?: string): Pro case Usage.wrap: { algo = { - name: consts.ENCRYPTION, - length: 256 + name: ENCRYPTION_ALGORITHM, + length: BIT_LENGTH() } usages = ['wrapKey', 'unwrapKey'] info_txt = 'wrap' @@ -77,7 +73,7 @@ export async function hkdf(key: Uint8Array, usage: Usage, context?: string): Pro return await crypto.subtle.deriveKey( { name: 'HKDF', - hash: 'SHA-512', + hash: HASH_NAME(), salt: new Uint8Array(16), info }, @@ -91,8 +87,7 @@ export async function hkdf(key: Uint8Array, usage: Usage, context?: string): Pro /** Minimum salt size : 16 bytes */ export async function pbkdf( salt: Uint8Array, - password: string, - strength: Strength = Strength.moderate + password: string ): Promise { log.trace('PBKDF') @@ -107,7 +102,7 @@ export async function pbkdf( ]) let iterations: number - switch (strength) { + switch (STRENGTH()) { case Strength.weak: iterations = 100_000 break @@ -128,10 +123,10 @@ export async function pbkdf( name: 'PBKDF2', salt, iterations, - hash: 'SHA-512' + hash: HASH_NAME() }, material, - 256 + BIT_LENGTH() ) return new Uint8Array(buffer) @@ -148,11 +143,11 @@ export async function ecdh( try { seed_bits = await crypto.subtle.deriveBits( { - name: consts.ECDH.name, + name: 'ECDH', public: pubkey }, privkey, - 256 + BIT_LENGTH() ) } catch (e) { log.warn('Failed to derive bits from keypair') @@ -166,14 +161,14 @@ export async function ecdh( return await crypto.subtle.deriveKey( { name: 'HKDF', - hash: 'SHA-512', + hash: HASH_NAME(), salt: new Uint8Array(16), info: new TextEncoder().encode('ECDH-' + (context ?? 'key')) }, seed, { - name: consts.ENCRYPTION, - length: 256 + name: ENCRYPTION_ALGORITHM, + length: BIT_LENGTH() }, false, outputUsage diff --git a/src/misc.ts b/src/misc.ts index adaf799..0c84929 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -1,6 +1,6 @@ import logger from 'log' import {a2b64, b642a} from 'misc' -import * as consts from './const' +import {ECDH_PARAMETERS, SIGNATURE_KEY} from './const' const log = logger('misc') @@ -24,7 +24,7 @@ export async function pubkey_fromString(pubkey_str: string, usage: Usage): Promi return await crypto.subtle.importKey( 'spki', pubkey, - usage === Usage.ecdh ? consts.ECDH : consts.SIGNATURE_KEY, + usage === Usage.ecdh ? ECDH_PARAMETERS() : SIGNATURE_KEY(), true, usage === Usage.ecdh ? [] : ['verify'] ) diff --git a/src/private-box.ts b/src/private-box.ts index 7cb34cf..1c947d7 100644 --- a/src/private-box.ts +++ b/src/private-box.ts @@ -1,8 +1,8 @@ import logger from 'log' import SecretBox from './secret-box' import {DHusage, ecdh} from './kdf' -import * as consts from './const' -import * as misc from './misc.ts' +import {ECDH_PARAMETERS} from './const' +import * as misc from './misc' const log = logger('crypto:private-box') @@ -14,7 +14,7 @@ export default class PrivateBox { public static async gen(extractable: boolean = true): Promise { log.trace('generate keypair') - return await crypto.subtle.generateKey(consts.ECDH, extractable, ['deriveBits']) + return await crypto.subtle.generateKey(ECDH_PARAMETERS(), extractable, ['deriveBits']) } public static async encrypt(data: Uint8Array, pubkey: CryptoKey, context?: string): Promise { diff --git a/src/private-wrap.ts b/src/private-wrap.ts index 893680e..a665204 100644 --- a/src/private-wrap.ts +++ b/src/private-wrap.ts @@ -1,8 +1,8 @@ import logger from 'log' import {DHusage, ecdh} from './kdf' import SecretWrap from './secret-wrap' +import {ECDH_PARAMETERS} from './const' import * as misc from './misc' -import * as consts from './const' const log = logger('crypto:private-wrap') @@ -14,7 +14,7 @@ export default class PrivateWrap { public static async gen(extractable: boolean = true): Promise { log.trace('generate keypair') - return (await crypto.subtle.generateKey(consts.ECDH, extractable, ['deriveBits'])) as CryptoKeyPair + return await crypto.subtle.generateKey(ECDH_PARAMETERS(), extractable, ['deriveBits']) } public static async wrap(data: CryptoKey, pubkey: CryptoKey, context?: string): Promise { diff --git a/src/secret-box.ts b/src/secret-box.ts index 83d5be2..c829ed9 100644 --- a/src/secret-box.ts +++ b/src/secret-box.ts @@ -1,6 +1,6 @@ import logger from 'log' import {a2b64, b642a} from 'misc' -import * as consts from './const' +import {BIT_LENGTH, ENCRYPTION_ALGORITHM} from './const' const log = logger('crypto:secret-box') @@ -14,8 +14,8 @@ export default class SecretBox { log.trace('generate key') return await crypto.subtle.generateKey( { - name: consts.ENCRYPTION, - length: 256 + name: ENCRYPTION_ALGORITHM, + length: BIT_LENGTH() }, extractable, ['encrypt', 'decrypt'] @@ -31,7 +31,7 @@ export default class SecretBox { try { cipher = await crypto.subtle.encrypt( { - name: consts.ENCRYPTION, + name: ENCRYPTION_ALGORITHM, iv }, key, @@ -50,7 +50,7 @@ export default class SecretBox { try { const buffer = await crypto.subtle.decrypt( { - name: consts.ENCRYPTION, + name: ENCRYPTION_ALGORITHM, iv: this.iv }, key, diff --git a/src/secret-wrap.ts b/src/secret-wrap.ts index cf17a6f..3d1c712 100644 --- a/src/secret-wrap.ts +++ b/src/secret-wrap.ts @@ -1,6 +1,6 @@ import logger from 'log' import {a2b64, b642a} from 'misc' -import * as consts from './const' +import {BIT_LENGTH, ENCRYPTION_ALGORITHM} from './const' const log = logger('crypto:secret-wrap') @@ -17,8 +17,8 @@ export default class SecretWrap { log.trace('generate key') return await crypto.subtle.generateKey( { - name: consts.ENCRYPTION, - length: 256 + name: ENCRYPTION_ALGORITHM, + length: BIT_LENGTH() }, extractable, ['wrapKey', 'unwrapKey'] @@ -47,7 +47,7 @@ export default class SecretWrap { let box: ArrayBuffer try { - box = await crypto.subtle.wrapKey(format, data, key, {name: consts.ENCRYPTION, iv}) + box = await crypto.subtle.wrapKey(format, data, key, {name: ENCRYPTION_ALGORITHM, iv}) } catch (e) { log.warn('Failed to wrap key') log.debug('Error :', e) @@ -63,7 +63,7 @@ export default class SecretWrap { this.type, this.cipher, key, - {name: consts.ENCRYPTION, iv: this.iv}, + {name: ENCRYPTION_ALGORITHM, iv: this.iv}, this.algorithm, true, this.usages diff --git a/src/signature.ts b/src/signature.ts index 38597cf..73e6562 100644 --- a/src/signature.ts +++ b/src/signature.ts @@ -1,18 +1,19 @@ import logger from 'log' -const log = logger('crypto:signature') -import * as consts from './const' +import {SIGNATURE_KEY, SIGNATURE_PARAMETERS} from './const' +import {a2b64, b642a} from 'misc' import * as misc from './misc' -import * as libmisc from 'misc' + +const log = logger('crypto:signature') export async function gen(extractable: boolean = true): Promise { - return await crypto.subtle.generateKey(consts.SIGNATURE_KEY, extractable, ['sign', 'verify']) + return await crypto.subtle.generateKey(SIGNATURE_KEY(), extractable, ['sign', 'verify']) } export async function sign(message: Uint8Array, privkey: CryptoKey): Promise { log.trace('sign') let buffer: ArrayBuffer try { - buffer = await crypto.subtle.sign(consts.SIGNATURE_ALGO, privkey, message) + buffer = await crypto.subtle.sign(SIGNATURE_PARAMETERS(), privkey, message) } catch (e) { log.warn('Failed to sign') log.debug('Error :', e) @@ -24,7 +25,7 @@ export async function sign(message: Uint8Array, privkey: CryptoKey): Promise { log.trace('verify') try { - return await crypto.subtle.verify(consts.SIGNATURE_ALGO, pubkey, signature, message) + return await crypto.subtle.verify(SIGNATURE_PARAMETERS(), pubkey, signature, message) } catch (e) { log.warn('Failed to verify') log.debug('Error :', e) @@ -40,8 +41,8 @@ export async function pubkey_fromString(pubkey: string): Promise { await test_keys(k1!, k2!, usage1, true, false) }) test('Minimum size', async () => { - const k = await kdf.hkdf(new Uint8Array(31), usage1) + const k = await kdf.hkdf(new Uint8Array(BYTE_LENGTH() - 1), usage1) expect(k).toBeNull() }) }) @@ -114,9 +115,12 @@ describe('PBKDF', () => { expect(k1).not.toEqual(k2) }) test('Different strengths', async () => { - const k1 = await kdf.pbkdf(salt1, pwd2, kdf.Strength.weak) - const k2 = await kdf.pbkdf(salt1, pwd1, kdf.Strength.moderate) - const k3 = await kdf.pbkdf(salt1, pwd2, kdf.Strength.strong) + set_strength(Strength.weak) + const k1 = await kdf.pbkdf(salt1, pwd2) + set_strength(Strength.moderate) + const k2 = await kdf.pbkdf(salt1, pwd1) + set_strength(Strength.strong) + const k3 = await kdf.pbkdf(salt1, pwd2) expect(k1).not.toEqual(k2) expect(k1).not.toEqual(k3) expect(k2).not.toEqual(k3) diff --git a/test/misc.test.ts b/test/misc.test.ts index 69e0e37..6dbde25 100644 --- a/test/misc.test.ts +++ b/test/misc.test.ts @@ -39,11 +39,12 @@ test('Signature', 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_toString(k.publicKey) const de = await pubkey_fromString(ser, Usage.sign) expect(de).not.toBeNull() - const verification = await signature.verify(message, de!, signed) + const verification = await signature.verify(message, de!, signed!) expect(verification).toBeTrue() }) diff --git a/test/wrap.test.ts b/test/wrap.test.ts index 4fe27ef..113059a 100644 --- a/test/wrap.test.ts +++ b/test/wrap.test.ts @@ -26,7 +26,8 @@ test('Signature', async () => { const message = new Uint8Array(8) const signed = await signature.sign(message, privk) - const verification = await signature.verify(message, k.publicKey, signed) + expect(signed).not.toBeNull() + const verification = await signature.verify(message, k.publicKey, signed!) expect(verification).toBeTrue() }) test('SecretWrap', async () => {