Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/drivers/aes_256_cbc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>)[]
}

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 {
Expand Down
15 changes: 10 additions & 5 deletions src/drivers/aes_256_gcm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>)[]
}

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 {
Expand Down
14 changes: 9 additions & 5 deletions src/drivers/base_driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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>): 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[]) {
Expand Down
15 changes: 10 additions & 5 deletions src/drivers/chacha20_poly1305.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>)[]
}

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 {
Expand Down
18 changes: 1 addition & 17 deletions src/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/encryption_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<KnownEncrypters extends Record<string, EncryptionConfig>> {
/**
Expand Down
11 changes: 7 additions & 4 deletions src/message_verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<string>)[]) {
this.#cryptoKeys = secrets.map((s) =>
createHash('sha256')
.update(typeof s === 'string' ? s : s.release())
.digest()
)
}

/**
Expand Down
20 changes: 19 additions & 1 deletion src/types/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @copyright Boring Node
*/

import { type Secret } from '@poppinss/utils'

export type CypherText = `${string}.${string}.${string}.${string}`

/**
Expand Down Expand Up @@ -41,7 +43,7 @@ export interface EncryptionDriverContract {
export type ManagerDriverFactory = () => EncryptionDriverContract

export interface BaseConfig {
key: string
key: string | Secret<string>
}

export interface LegacyConfig extends BaseConfig {}
Expand All @@ -59,3 +61,19 @@ export type Config<KnownEncrypters extends Record<string, ManagerDriverFactory>>
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<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 | Secret<string>)[]
}
16 changes: 8 additions & 8 deletions tests/message_verifier.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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({}))
Expand All @@ -48,27 +48,27 @@ 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)

assert.deepEqual(unsigned, { username: 'virk' })
})

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)))
Expand Down
Loading