Skip to content

Crypto - Full Reference

Exhaustive reference for AES, RSA, ECDH, the shared AbstractCryptoAlgorithm/BaseCryptoAlgorithm base classes, and the standalone hash() utility. For a readable introduction and the most common tasks, start with the Crypto overview.

Files:

Quick Reference

ClassExtendsSecret typeAsyncRuntime API
AESBaseCryptoAlgorithmstringNoNode node:crypto
RSABaseCryptoAlgorithmstring (base64 DER key)NoNode node:crypto
ECDHAbstractCryptoAlgorithmCryptoKeyYesWeb Crypto (crypto.subtle)
hash()(standalone function)string (HMAC secret, SHA256 only)NoNode node:crypto

Import paths

typescript
// Algorithm classes
import { AES, RSA, ECDH } from '@venizia/ignis-helpers';

// Hash utility function
import { hash } from '@venizia/ignis-helpers';

// Types
import type {
  AESAlgorithmType,
  RSAAlgorithmType,
  ECDHAlgorithmType,
  IECDHEncryptedPayload,
  IECDHExtraOptions,
  ICryptoAlgorithm,
} from '@venizia/ignis-helpers';

All of the above resolve through the root @venizia/ignis-helpers barrel, which re-exports ./modules (and therefore ./modules/crypto) and ./utilities in full.

Type Hierarchy

Source -> base.algorithm.ts

ICryptoAlgorithm (interface)
  └── AbstractCryptoAlgorithm (extends BaseHelper)
        ├── BaseCryptoAlgorithm (adds normalizeSecretKey, getAlgorithmKeySize)
        │     ├── AES
        │     └── RSA
        └── ECDH (uses CryptoKey objects, not string secrets)

ICryptoAlgorithm carries 7 generic type parameters - the algorithm name literal, encrypt/decrypt input and output types, the secret key type, and an extra-options type:

typescript
interface ICryptoAlgorithm<
  AlgorithmNameType extends string,
  EncryptInputType = unknown,
  DecryptInputType = unknown,
  SecretKeyType = unknown,
  EncryptReturnType = unknown,
  DecryptReturnType = unknown,
  ExtraOptions = unknown,
> {
  algorithm: AlgorithmNameType;
  encrypt(opts: { message: EncryptInputType; secret: SecretKeyType; opts?: ExtraOptions }): EncryptReturnType;
  decrypt(opts: { message: DecryptInputType; secret: SecretKeyType; opts?: ExtraOptions }): DecryptReturnType;
}

AbstractCryptoAlgorithm extends BaseHelper and declares encrypt/decrypt as abstract; it adds no behavior of its own. BaseCryptoAlgorithm is the concrete base for string-secret algorithms:

MemberSignatureDescription
constructor(opts: { scope: string; algorithm: AlgorithmType })Sets this.algorithm, calls validateAlgorithmName
validateAlgorithmName(opts: { algorithm: AlgorithmType }) => voidThrows if algorithm is empty/falsy
normalizeSecretKey(opts: { secret: string; length: number; padEnd?: string }) => stringTruncates to length or right-pads with padEnd (default '0' - the digit character, since DEFAULT_PAD_END is (0x00).toString(), not a null byte)
getAlgorithmKeySize() => numberParses the bit size out of this.algorithm (e.g. 256 from 'aes-256-gcm'), divides by 8 for byte length

ECDH extends AbstractCryptoAlgorithm directly - it does not inherit normalizeSecretKey or getAlgorithmKeySize, since its secrets are CryptoKey objects, not strings.

AES

Source -> aes.algorithm.ts

typescript
type AESAlgorithmType = 'aes-256-cbc' | 'aes-256-gcm';

const aes = AES.withAlgorithm('aes-256-gcm'); // or 'aes-256-cbc'
ModeAuthenticatedNotes
aes-256-cbcNoPlain block cipher, no tamper detection
aes-256-gcmYesCiphertext includes a 16-byte GCM auth tag; tampering throws on decrypt

encrypt

typescript
encrypt(opts: { message: string; secret: string; opts?: IAESExtraOptions }): string
Option (opts.opts)TypeDefaultDescription
ivBuffercrypto.randomBytes(16)Initialization vector
inputEncodingcrypto.Encoding'utf-8'Encoding of message
outputEncodingcrypto.Encoding'base64'Encoding of the returned ciphertext
doThrowbooleantrueIf false, returns the original message instead of throwing on error

The secret is normalized via normalizeSecretKey to the algorithm's key size (32 bytes for both modes) before being used as the cipher key. The output layout is IV [+ GCM auth tag] + ciphertext, concatenated and encoded with outputEncoding.

typescript
import C from 'node:crypto';

const encrypted = aes.encrypt({
  message: 'hello',
  secret: 'my-secret',
  opts: {
    iv: C.randomBytes(16),
    inputEncoding: 'utf-8',
    outputEncoding: 'hex',
    doThrow: false,
  },
});

decrypt

typescript
decrypt(opts: { message: string; secret: string; opts?: IAESExtraOptions }): string
Option (opts.opts)TypeDefaultDescription
ivBufferExtracted from the first 16 bytes of the decoded ciphertextInitialization vector
inputEncodingcrypto.Encoding'base64'Encoding of message
outputEncodingcrypto.Encoding'utf-8'Encoding of the returned plaintext
doThrowbooleantrueIf false, returns the original message instead of throwing on error

For aes-256-gcm, the next 16 bytes after the IV are read as the auth tag and passed to setAuthTag before the remaining bytes are treated as ciphertext.

WARNING

Decrypting aes-256-gcm ciphertext with an aes-256-cbc instance (or vice versa) throws Unsupported state or unable to authenticate data - the two modes produce incompatible byte layouts. Always encrypt and decrypt with the same algorithm mode.

encryptFile / decryptFile

typescript
encryptFile(opts: { absolutePath: string; secret: string }): string
decryptFile(opts: { absolutePath: string; secret: string }): string

Both read the file synchronously via fs.readFileSync, decode it as UTF-8, then call encrypt/decrypt on the string content using default extra-options. If absolutePath is empty or falsy, both return '' without touching the filesystem.

typescript
const encrypted = aes.encryptFile({ absolutePath: '/path/to/config.json', secret: 'my-secret' });
const decrypted = aes.decryptFile({ absolutePath: '/path/to/config.json.enc', secret: 'my-secret' });

RSA

Source -> rsa.algorithm.ts

typescript
type RSAAlgorithmType = 'rsa';

const rsa = RSA.withAlgorithm(); // no parameters - always algorithm 'rsa'

generateDERKeyPair

typescript
generateDERKeyPair(opts?: { modulus: number }): { publicKey: Buffer; privateKey: Buffer }
OptionTypeDefaultDescription
modulusnumber2048RSA modulus length in bits, passed to crypto.generateKeyPairSync

publicKey is exported as { type: 'spki', format: 'der' }; privateKey as { type: 'pkcs8', format: 'der' }. Both are raw Buffers - base64-encode them (.toString('base64')) to pass as the secret string to encrypt/decrypt.

encrypt

typescript
encrypt(opts: { message: string; secret: string; opts?: IRSAExtraOptions }): string

secret is the base64-encoded public key (SPKI/DER). Internally builds a public key via crypto.createPublicKey({ format: 'der', type: 'spki' }) and calls crypto.publicEncrypt.

Option (opts.opts)TypeDefaultDescription
inputEncoding.keycrypto.Encoding'base64'Encoding of the secret key buffer
inputEncoding.messagecrypto.Encoding'utf-8'Encoding of message
outputEncodingcrypto.Encoding'base64'Encoding of the returned ciphertext
doThrowbooleantrueIf false, returns the original message instead of throwing on error

decrypt

typescript
decrypt(opts: { message: string; secret: string; opts?: IRSAExtraOptions }): string

secret is the base64-encoded private key (PKCS8/DER). Internally builds a private key via crypto.createPrivateKey({ format: 'der', type: 'pkcs8' }) and calls crypto.privateDecrypt.

Option (opts.opts)TypeDefaultDescription
inputEncoding.keycrypto.Encoding'base64'Encoding of the secret key buffer
inputEncoding.messagecrypto.Encoding'base64'Encoding of message
outputEncodingcrypto.Encoding'utf-8'Encoding of the returned plaintext
doThrowbooleantrueIf false, returns the original message instead of throwing on error
typescript
const rsa = RSA.withAlgorithm();
const { publicKey, privateKey } = rsa.generateDERKeyPair({ modulus: 4096 });

const encrypted = rsa.encrypt({
  message: 'hello',
  secret: publicKey.toString('base64'),
  opts: { outputEncoding: 'hex', doThrow: false },
});

const decrypted = rsa.decrypt({
  message: encrypted,
  secret: privateKey.toString('base64'),
  opts: { inputEncoding: { key: 'base64', message: 'hex' } },
});

ECDH Key Exchange

Source -> ecdh.algorithm.ts

ECDH implements ECDH P-256 key exchange with HKDF-derived AES-256-GCM session encryption, entirely on the Web Crypto API (crypto.subtle). Every method is async.

typescript
const ecdh = ECDH.withAlgorithm(); // curve is always 'ecdh-p256'
const ecdhCustom = ECDH.withAlgorithm({ hkdfInfo: 'my-app-session-keys' });
Constructor optionTypeDefaultDescription
algorithm'ecdh-p256''ecdh-p256'Accepted but not branched on - the curve is always P-256
hkdfInfostring'ignis-ecdh-p256-aes-256-gcm-v1'HKDF info string, UTF-8 encoded, used to isolate key derivation between application contexts

Different hkdfInfo values produce incompatible derived keys from the same ECDH shared secret.

Key generation and import

MethodSignatureDescription
generateKeyPair() => Promise<{ keyPair: CryptoKeyPair; publicKeyB64: string }>deriveBits-only key pair (extractable: false); publicKeyB64 is the raw exported public key, base64-encoded (65 bytes for P-256)
importPublicKey(opts: { rawKeyB64: string }) => Promise<CryptoKey>Imports a peer's raw base64 public key
typescript
const { keyPair, publicKeyB64 } = await ecdh.generateKeyPair();
const peerKey = await ecdh.importPublicKey({ rawKeyB64: peerPublicKeyB64 });

deriveAESKey

typescript
deriveAESKey(opts: {
  privateKey: CryptoKey;
  peerPublicKey: CryptoKey;
  salt?: string;
}): Promise<{ key: CryptoKey; salt: string }>

Derives shared bits via ECDH (deriveBits, 256 bits), imports them as an HKDF key, then derives a non-extractable AES-256-GCM CryptoKey via HKDF-SHA256 using salt and the instance's hkdfInfo.

OptionTypeDefaultDescription
privateKeyCryptoKey-Caller's ECDH private key from generateKeyPair()
peerPublicKeyCryptoKey-Peer's public key from importPublicKey()
saltstringA random 32-byte salt is generatedBase64-encoded HKDF salt. Omit to generate a new random one

IMPORTANT

Both parties must use the same salt to derive matching keys. The initiator omits salt (a random one is generated and returned); the responder must pass that returned salt back into their own deriveAESKey call. If both sides generate their own salt, they derive different, non-matching keys.

encrypt / decrypt

typescript
encrypt(opts: { message: string; secret: CryptoKey; opts?: IECDHExtraOptions }): Promise<IECDHEncryptedPayload>
decrypt(opts: { message: IECDHEncryptedPayload; secret: CryptoKey; opts?: IECDHExtraOptions }): Promise<string>

secret is the CryptoKey returned by deriveAESKey. Uses AES-GCM with a random 12-byte IV per call and a 128-bit auth tag.

typescript
interface IECDHEncryptedPayload {
  iv: string; // base64 encoded 12-byte IV
  ct: string; // base64 encoded ciphertext + 128-bit GCM auth tag
}

interface IECDHExtraOptions {
  additionalData?: string;
}

opts.additionalData (AAD) is authenticated but not encrypted - it binds the ciphertext to a context (channel ID, session ID) so the ciphertext cannot be replayed in a different context. Decrypt must supply the exact same additionalData; a mismatch (including an omitted value where one was used to encrypt) throws.

Complete flow

typescript
const ecdh = ECDH.withAlgorithm();

// 1. Both parties generate key pairs
const alice = await ecdh.generateKeyPair();
const bob = await ecdh.generateKeyPair();

// 2. Exchange public keys (safe to share over any channel)
const alicePubForBob = await ecdh.importPublicKey({ rawKeyB64: alice.publicKeyB64 });
const bobPubForAlice = await ecdh.importPublicKey({ rawKeyB64: bob.publicKeyB64 });

// 3. Initiator derives the AES key (generates a random salt)
const { key: aliceKey, salt } = await ecdh.deriveAESKey({
  privateKey: alice.keyPair.privateKey,
  peerPublicKey: bobPubForAlice,
});

// 4. Responder derives the SAME AES key using the initiator's salt
const { key: bobKey } = await ecdh.deriveAESKey({
  privateKey: bob.keyPair.privateKey,
  peerPublicKey: alicePubForBob,
  salt,
});

// 5. Alice encrypts, Bob decrypts (or vice versa)
const encrypted = await ecdh.encrypt({ message: 'Hello Bob!', secret: aliceKey });
const decrypted = await ecdh.decrypt({ message: encrypted, secret: bobKey });
// => 'Hello Bob!'

Security properties

PropertyGuarantee
ConfidentialityAES-256-GCM encryption
IntegrityGCM authentication tag - tampered ciphertext is detected on decrypt
Forward secrecyEphemeral, non-extractable key pairs - compromising one session does not compromise others
Key isolationhkdfInfo separates key derivation across application contexts
Context bindingadditionalData (AAD) prevents cross-context replay

Hashing

Source -> crypto.utility.ts

hash() is a standalone function - not a class, not part of the AES/RSA/ECDH hierarchy - exported from packages/helpers/src/utilities, not modules/crypto. It is documented in full on the Crypto Utility reference.

typescript
function hash(
  text: string,
  options: {
    algorithm: 'SHA256' | 'MD5';
    secret?: string;
    outputType: C.BinaryToTextEncoding; // 'hex' | 'base64' | 'base64url' | 'latin1'
  },
): string;
OptionTypeRequiredDescription
algorithm'SHA256' | 'MD5'YesHashing algorithm
secretstringOnly for SHA256HMAC secret key; ignored for MD5
outputType'hex' | 'base64' | 'base64url' | 'latin1'YesDigest output encoding

WARNING

SHA256 uses createHmac and requires secret. If secret is undefined, the function short-circuits and returns text unchanged - no hash is computed, no error is thrown. MD5 never needs a secret and always hashes.

typescript
import { hash } from '@venizia/ignis-helpers';

const md5Hash = hash('some text', { algorithm: 'MD5', outputType: 'hex' });
const sha256Hmac = hash('some text', { algorithm: 'SHA256', secret: 'a-secret-key', outputType: 'hex' });

API Summary

MethodClassReturnsDescription
AES.withAlgorithm(algorithm)AESAESCreate an AES instance with CBC or GCM mode
encrypt(opts)AESstringEncrypt a string message
decrypt(opts)AESstringDecrypt a ciphertext string
encryptFile(opts)AESstringEncrypt file contents to a string
decryptFile(opts)AESstringDecrypt file contents to a string
RSA.withAlgorithm()RSARSACreate an RSA instance
generateDERKeyPair(opts?)RSA{ publicKey: Buffer; privateKey: Buffer }Generate a DER-format key pair
encrypt(opts)RSAstringEncrypt with a public key
decrypt(opts)RSAstringDecrypt with a private key
ECDH.withAlgorithm(opts?)ECDHECDHCreate an ECDH instance with optional hkdfInfo
generateKeyPair()ECDHPromise<{ keyPair: CryptoKeyPair; publicKeyB64: string }>Generate a P-256 key pair
importPublicKey(opts)ECDHPromise<CryptoKey>Import a peer's base64 public key
deriveAESKey(opts)ECDHPromise<{ key: CryptoKey; salt: string }>Derive an AES-256-GCM key via HKDF
encrypt(opts)ECDHPromise<IECDHEncryptedPayload>Encrypt with a derived AES key
decrypt(opts)ECDHPromise<string>Decrypt with a derived AES key
hash(text, options)(function)stringMD5 digest or HMAC-SHA256

Troubleshooting

"[validateAlgorithmName] Invalid algorithm name | algorithm: undefined"

Cause: An empty or undefined algorithm was passed to the constructor (or withAlgorithm()).

Fix:

typescript
const aes = AES.withAlgorithm('aes-256-gcm'); // not undefined or empty
const rsa = RSA.withAlgorithm();               // no parameter needed
const ecdh = ECDH.withAlgorithm();             // no parameter needed

"[ECDH.fromBase64] Invalid base64 input"

Cause: A value passed to an ECDH method (public key, salt, IV, or ciphertext) is not valid base64 - length not divisible by 4, or characters outside A-Za-z0-9+/=.

Fix: Pass base64 strings through exactly as produced by the methods that generated them (publicKeyB64, salt, iv, ct) - do not trim, re-encode, or modify them.

"Unsupported state or unable to authenticate data"

Cause: The ciphertext or auth tag was modified in transit, or you decrypted aes-256-gcm ciphertext with a aes-256-cbc instance (or vice versa) - the two modes produce incompatible byte layouts.

Fix: Use the same algorithm mode for both encrypt and decrypt.

ECDH decrypt throws even though both sides imported each other's public keys

Cause: Each deriveAESKey call without a salt generates a new random 32-byte salt. If both sides generate their own, they derive different AES keys.

Fix: The initiator calls deriveAESKey without salt and sends the returned salt to the responder; the responder passes that exact salt into their own deriveAESKey call.

SHA256 hash returns the original text instead of a hash

Cause: hash() with algorithm: 'SHA256' requires secret; when secret is undefined it short-circuits and returns text unchanged.

Fix:

typescript
const hashed = hash('text', { algorithm: 'SHA256', secret: 'my-hmac-key', outputType: 'hex' });

See also