diff --git a/src/drivers/aes_256_cbc.ts b/src/drivers/aes_256_cbc.ts index 848155f..d7fd3b6 100644 --- a/src/drivers/aes_256_cbc.ts +++ b/src/drivers/aes_256_cbc.ts @@ -6,23 +6,28 @@ */ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto' -import { MessageBuilder } from '@poppinss/utils' +import { MessageBuilder, type Secret } from '@poppinss/utils' import { BaseDriver } from './base_driver.ts' import { Hmac } from '../hmac.ts' import * as errors from '../exceptions.ts' -import type { AES256CBCConfig, CypherText, EncryptionDriverContract } from '../types/main.ts' +import type { + AES256CBCConfig, + CypherText, + EncryptionConfig, + EncryptionDriverContract, +} from '../types/main.ts' import { base64UrlDecode, base64UrlEncode } from '../base64.ts' export interface AES256CBCDriverConfig { id: string - keys: string[] + keys: (string | Secret)[] } export function aes256cbc(config: AES256CBCDriverConfig) { return { - driver: (key: string) => new AES256CBC({ id: config.id, key }), + driver: (key) => new AES256CBC({ id: config.id, key }), keys: config.keys, - } + } satisfies EncryptionConfig } export class AES256CBC extends BaseDriver implements EncryptionDriverContract { diff --git a/src/drivers/aes_256_gcm.ts b/src/drivers/aes_256_gcm.ts index 597ddc8..ce0c93f 100644 --- a/src/drivers/aes_256_gcm.ts +++ b/src/drivers/aes_256_gcm.ts @@ -6,22 +6,27 @@ */ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' -import { MessageBuilder } from '@poppinss/utils' +import { MessageBuilder, type Secret } from '@poppinss/utils' import { BaseDriver } from './base_driver.ts' import * as errors from '../exceptions.ts' import { base64UrlDecode, base64UrlEncode } from '../base64.ts' -import type { AES256GCMConfig, CypherText, EncryptionDriverContract } from '../types/main.ts' +import type { + AES256GCMConfig, + CypherText, + EncryptionConfig, + EncryptionDriverContract, +} from '../types/main.ts' export interface AES256GCMDriverConfig { id: string - keys: string[] + keys: (string | Secret)[] } export function aes256gcm(config: AES256GCMDriverConfig) { return { - driver: (key: string) => new AES256GCM({ id: config.id, key }), + driver: (key) => new AES256GCM({ id: config.id, key }), keys: config.keys, - } + } satisfies EncryptionConfig } export class AES256GCM extends BaseDriver implements EncryptionDriverContract { diff --git a/src/drivers/base_driver.ts b/src/drivers/base_driver.ts index b483676..455815f 100644 --- a/src/drivers/base_driver.ts +++ b/src/drivers/base_driver.ts @@ -8,6 +8,7 @@ import { createHash } from 'node:crypto' import * as errors from '../exceptions.ts' import type { BaseConfig, CypherText } from '../types/main.ts' +import { type Secret } from '@poppinss/utils' export abstract class BaseDriver { /** @@ -23,21 +24,24 @@ export abstract class BaseDriver { separator = '.' protected constructor(config: BaseConfig) { - this.#validateSecret(config.key) - this.cryptoKey = createHash('sha256').update(config.key).digest() + const key = this.#validateAndGetSecret(config.key) + this.cryptoKey = createHash('sha256').update(key).digest() } /** - * Validates the app secret + * Validates the app secret and returns it back as a string */ - #validateSecret(secret: string) { + #validateAndGetSecret(secret: string | Secret): string { if (!secret) { throw new errors.E_MISSING_ENCRYPTER_KEY() } - if (secret.length < 16) { + const revealedSecret = typeof secret === 'string' ? secret : secret.release() + if (revealedSecret.length < 16) { throw new errors.E_INSECURE_ENCRYPTER_KEY() } + + return revealedSecret } protected computeReturns(values: string[]) { diff --git a/src/drivers/chacha20_poly1305.ts b/src/drivers/chacha20_poly1305.ts index c5907c7..93b6aae 100644 --- a/src/drivers/chacha20_poly1305.ts +++ b/src/drivers/chacha20_poly1305.ts @@ -6,22 +6,27 @@ */ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' -import { MessageBuilder } from '@poppinss/utils' +import { MessageBuilder, type Secret } from '@poppinss/utils' import { BaseDriver } from './base_driver.ts' import * as errors from '../exceptions.ts' -import type { ChaCha20Poly1305Config, CypherText, EncryptionDriverContract } from '../types/main.ts' +import type { + ChaCha20Poly1305Config, + CypherText, + EncryptionConfig, + EncryptionDriverContract, +} from '../types/main.ts' import { base64UrlDecode, base64UrlEncode } from '../base64.ts' export interface ChaCha20Poly1305DriverConfig { id: string - keys: string[] + keys: (string | Secret)[] } export function chacha20poly1305(config: ChaCha20Poly1305DriverConfig) { return { - driver: (key: string) => new ChaCha20Poly1305({ id: config.id, key }), + driver: (key) => new ChaCha20Poly1305({ id: config.id, key }), keys: config.keys, - } + } satisfies EncryptionConfig } export class ChaCha20Poly1305 extends BaseDriver implements EncryptionDriverContract { diff --git a/src/encryption.ts b/src/encryption.ts index 2eff762..0300abb 100644 --- a/src/encryption.ts +++ b/src/encryption.ts @@ -6,23 +6,7 @@ */ import { MessageVerifier } from './message_verifier.ts' -import type { CypherText, EncryptionDriverContract } from './types/main.ts' - -/** - * Configuration for the Encryption class - */ -export interface EncryptionConfig { - /** - * Factory function that creates a driver instance for a given key - */ - driver: (key: string) => EncryptionDriverContract - - /** - * List of keys to use for encryption/decryption. - * The first key is used for encryption, all keys are tried for decryption. - */ - keys: string[] -} +import type { CypherText, EncryptionConfig, EncryptionDriverContract } from './types/main.ts' /** * Encryption class that wraps a driver and manages multiple keys. diff --git a/src/encryption_manager.ts b/src/encryption_manager.ts index 98b96f9..4b89554 100644 --- a/src/encryption_manager.ts +++ b/src/encryption_manager.ts @@ -7,9 +7,9 @@ import { RuntimeException } from '@poppinss/utils/exception' import debug from './debug.ts' -import { Encryption, type EncryptionConfig } from './encryption.ts' +import { Encryption } from './encryption.ts' import type { MessageVerifier } from './message_verifier.ts' -import type { CypherText } from './types/main.ts' +import type { CypherText, EncryptionConfig } from './types/main.ts' export class EncryptionManager> { /** diff --git a/src/message_verifier.ts b/src/message_verifier.ts index 00ee677..7d78c3e 100644 --- a/src/message_verifier.ts +++ b/src/message_verifier.ts @@ -6,7 +6,7 @@ */ import { createHash } from 'node:crypto' -import { MessageBuilder } from '@poppinss/utils' +import { MessageBuilder, type Secret } from '@poppinss/utils' import { RuntimeException } from '@poppinss/utils/exception' import { base64UrlEncode, base64UrlDecode } from './base64.ts' import { Hmac } from './hmac.ts' @@ -31,9 +31,12 @@ export class MessageVerifier { */ #separator = '.' - constructor(secret: string | string[]) { - const secrets = Array.isArray(secret) ? secret : [secret] - this.#cryptoKeys = secrets.map((s) => createHash('sha256').update(s).digest()) + constructor(secrets: (string | Secret)[]) { + this.#cryptoKeys = secrets.map((s) => + createHash('sha256') + .update(typeof s === 'string' ? s : s.release()) + .digest() + ) } /** diff --git a/src/types/main.ts b/src/types/main.ts index 062098d..fc6da56 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -5,6 +5,8 @@ * @copyright Boring Node */ +import { type Secret } from '@poppinss/utils' + export type CypherText = `${string}.${string}.${string}.${string}` /** @@ -41,7 +43,7 @@ export interface EncryptionDriverContract { export type ManagerDriverFactory = () => EncryptionDriverContract export interface BaseConfig { - key: string + key: string | Secret } export interface LegacyConfig extends BaseConfig {} @@ -59,3 +61,19 @@ export type Config> default?: keyof KnownEncrypters list: KnownEncrypters } + +/** + * Configuration for the Encryption class + */ +export interface EncryptionConfig { + /** + * Factory function that creates a driver instance for a given key + */ + driver: (key: string | Secret) => EncryptionDriverContract + + /** + * List of keys to use for encryption/decryption. + * The first key is used for encryption, all keys are tried for decryption. + */ + keys: (string | Secret)[] +} diff --git a/tests/message_verifier.spec.ts b/tests/message_verifier.spec.ts index 246adba..b929112 100644 --- a/tests/message_verifier.spec.ts +++ b/tests/message_verifier.spec.ts @@ -13,21 +13,21 @@ const SECRET = 'averylongradom32charactersstring' test.group('MessageVerifier', () => { test('disallow signing null and undefined values', ({ assert }) => { - const messageVerifier = new MessageVerifier(SECRET) + const messageVerifier = new MessageVerifier([SECRET]) assert.throws(() => messageVerifier.sign(null), 'Cannot sign "null" value') assert.throws(() => messageVerifier.sign(undefined), 'Cannot sign "undefined" value') }) test('sign an object using a secret', ({ assert }) => { - const messageVerifier = new MessageVerifier(SECRET) + const messageVerifier = new MessageVerifier([SECRET]) const signed = messageVerifier.sign({ username: 'virk' }) assert.equal(base64UrlDecode(signed.split('.')[0], 'utf8'), '{"message":{"username":"virk"}}') }) test('sign an object with purpose', ({ assert }) => { - const messageVerifier = new MessageVerifier(SECRET) + const messageVerifier = new MessageVerifier([SECRET]) const signed = messageVerifier.sign({ username: 'virk' }, undefined, 'login') assert.equal( @@ -37,7 +37,7 @@ test.group('MessageVerifier', () => { }) test('return null when unsigning non-string values', ({ assert }) => { - const messageVerifier = new MessageVerifier(SECRET) + const messageVerifier = new MessageVerifier([SECRET]) // @ts-expect-error assert.isNull(messageVerifier.unsign({})) @@ -48,7 +48,7 @@ test.group('MessageVerifier', () => { }) test('unsign value', ({ assert }) => { - const messageVerifier = new MessageVerifier(SECRET) + const messageVerifier = new MessageVerifier([SECRET]) const signed = messageVerifier.sign({ username: 'virk' }) const unsigned = messageVerifier.unsign(signed) @@ -56,19 +56,19 @@ test.group('MessageVerifier', () => { }) test('return null when unable to decode it', ({ assert }) => { - const messageVerifier = new MessageVerifier(SECRET) + const messageVerifier = new MessageVerifier([SECRET]) assert.isNull(messageVerifier.unsign('hello.world')) }) test('return null when hash separator is missing', ({ assert }) => { - const messageVerifier = new MessageVerifier(SECRET) + const messageVerifier = new MessageVerifier([SECRET]) assert.isNull(messageVerifier.unsign('helloworld')) }) test('return null when hash was touched', ({ assert }) => { - const messageVerifier = new MessageVerifier(SECRET) + const messageVerifier = new MessageVerifier([SECRET]) const signed = messageVerifier.sign({ username: 'virk' }) assert.isNull(messageVerifier.unsign(signed.slice(0, -2)))