NestJS library for AES-256-GCM field encryption, key rotation, optional KMS / hybrid envelope drivers, blind indexing (HMAC-SHA256), and HTTP interceptors driven by @Encrypted() metadata.
- Node.js ≥ 20 (LTS; recommended 22 — see
.nvmrc) - NestJS ≥ 9 (
@nestjs/common,@nestjs/core) reflect-metadata,class-transformer(peer dependencies)- Optional peers (install only what you use):
- AWS KMS:
@aws-sdk/client-kms - GCP KMS:
@google-cloud/kms
- AWS KMS:
- Local driver (default): symmetric encryption with a key ring (
keys); first entry encrypts; decrypt tries keys in order for rotation. - AWS KMS:
GenerateDataKey+ AES-GCM payload;v1:envelope with wrapped DEK. Subpathcryptonest/aws-kmsavoids loading the AWS SDK for local-only apps. - Google Cloud KMS: local DEK encrypted with KMS;
v2:gcp-kms:envelope. Subpathcryptonest/gcp-kms. OptionalgcpKms.logicalKeyIdfor the envelope key id segment (defaults tokeyName). - HashiCorp Vault Transit: data key from Transit + AES-GCM;
v2:vault-transit:envelope. Subpathcryptonest/vault-transit. - Hybrid RSA (RSA-OAEP-256 + AES-GCM):
v2:rsa-oaep-256:envelope; key ring orhybridRsaPEM defaults. Subpathcryptonest/hybrid-rsa. - Blind indexing:
EncryptionService.hashSearchTerm()/KeyRotationManager.generateBlindIndex()whenblindIndexSecret(≥ 32 bytes) is set. On DTOs,@Encrypted({ blindIndex: true })(or{ blindIndex: 'yourPropName' }) sets a companion HMAC field ontoPlain(response encryption). - DTOs & interceptors:
@Encrypted()on properties;@EncryptedRoute({ bodyDto, responseDto })withDecryptionInterceptor/EncryptionInterceptor. Nested objects are supported whenclass-transformer’s@Type(() => ChildDto)is set; depth is capped byinterceptorMaxDepth(default 8, viaEncryptionModuleoptions). EncryptionModule: global dynamic module withforRootandforRootAsync.
npm install cryptonest @nestjs/common @nestjs/core class-transformer reflect-metadata
# Optional:
npm install @aws-sdk/client-kms
npm install @google-cloud/kmsBuild before local linking: npm run build (outputs dist/).
EncryptionModule is @Global(). Use forRoot or forRootAsync.
import { EncryptionModule } from 'cryptonest';
@Module({
imports: [
EncryptionModule.forRoot({
keys: [
{ id: 'k2', secret: process.env.ENCRYPTION_KEY! }, // current — min 32 bytes
{ id: 'k1', secret: process.env.OLD_ENCRYPTION_KEY! }, // previous — tried on decrypt
],
blindIndexSecret: process.env.BLIND_INDEX_SECRET, // optional; ≥ 32 bytes if set
interceptorMaxDepth: 8, // optional; nested @Type DTO walk depth (default 8)
}),
],
})
export class AppModule {}EncryptionModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
keys: [{ id: 'k1', secret: config.getOrThrow('ENCRYPTION_KEY') }],
blindIndexSecret: config.get('BLIND_INDEX_SECRET'),
}),
}),Envelope drivers do not use keys for crypto; pass keys: [] if you only use KMS. awsKms.logicalKeyId (optional) is written into the ciphertext envelope; awsKms.keyId is the CMK id/ARN for GenerateDataKey.
import { KMSClient } from '@aws-sdk/client-kms';
import { EncryptionModule } from 'cryptonest';
EncryptionModule.forRoot({
driver: 'aws-kms',
keys: [],
awsKms: {
keyId: process.env.KMS_KEY_ID!,
logicalKeyId: process.env.KMS_LOGICAL_KEY_ID, // optional; defaults to keyId
client: new KMSClient({}),
},
blindIndexSecret: process.env.BLIND_INDEX_SECRET,
}),Direct driver import (optional): import { createAwsKmsDriver } from 'cryptonest/aws-kms'.
Wire @google-cloud/kms to GcpKmsClientLike (exported from cryptonest), for example:
import { KeyManagementServiceClient } from '@google-cloud/kms';
import { EncryptionModule } from 'cryptonest';
import type { GcpKmsClientLike } from 'cryptonest';
const kms = new KeyManagementServiceClient();
const client: GcpKmsClientLike = {
async encrypt({ name, plaintext }) {
const [result] = await kms.encrypt({
name,
plaintext: Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext),
});
return { ciphertext: Buffer.from(result.ciphertext as Uint8Array) };
},
async decrypt({ name, ciphertext }) {
const [result] = await kms.decrypt({
name,
ciphertext,
});
return { plaintext: Buffer.from(result.plaintext as Uint8Array) };
},
};
EncryptionModule.forRoot({
driver: 'gcp-kms',
keys: [],
gcpKms: {
keyName:
'projects/…/locations/…/keyRings/…/cryptoKeys/…/cryptoKeyVersions/1',
logicalKeyId: 'optional-stable-id', // optional; defaults to keyName
client,
},
blindIndexSecret: process.env.BLIND_INDEX_SECRET,
});Subpath: cryptonest/gcp-kms.
Provide a small adapter implementing VaultTransitClientLike (datakeyPlaintext, decrypt). Subpath: cryptonest/vault-transit.
EncryptionModule.forRoot({
driver: 'vault-transit',
keys: [],
vaultTransit: {
transitKeyName: 'my-key',
client: vaultTransitAdapter,
},
blindIndexSecret: process.env.BLIND_INDEX_SECRET,
}),Use driver: 'hybrid-rsa' with keys (PEM in each secret) and/or hybridRsa.publicKey / hybridRsa.privateKey. Subpath: cryptonest/hybrid-rsa.
EncryptionModule.forRoot({
driver: 'hybrid-rsa',
keys: [{ id: 'k1', secret: process.env.RSA_PRIVATE_KEY_PEM! }],
hybridRsa: { logicalKeyId: 'default-key' },
blindIndexSecret: process.env.BLIND_INDEX_SECRET,
}),Inject EncryptionService or KeyRotationManager.
import { EncryptionService } from 'cryptonest';
@Injectable()
export class UsersService {
constructor(private readonly encryption: EncryptionService) {}
async saveSsn(plain: string) {
const ciphertext = await this.encryption.encryptField(plain);
// persist ciphertext (v1 / v2 envelope)
}
async loadSsn(stored: string) {
return this.encryption.decryptField(stored);
}
}encryptField/decryptField: null/empty safe;decryptFieldonly decrypts values that look like library ciphertext (v1:orv2:prefixes); other strings are returned unchanged.hashSearchTerm: deterministic HMAC for equality queries (requiresblindIndexSecreton the module).
Lower-level KeyRotationManager.encrypt / .decrypt follow the configured driver (including full key-ring trial decrypt for local / hybrid).
const index = await this.encryption.hashSearchTerm(email);
// store `index` in e.g. email_blind_index
const search = await this.encryption.hashSearchTerm(userInput);
// query WHERE email_blind_index = searchFor responses, @Encrypted({ blindIndex: true }) encrypts the field and sets ${propertyName}BlindIndex to the same HMAC (plaintext must not already be ciphertext). Use { blindIndex: 'customColumnName' } to choose the companion key.
- Mark fields with
@Encrypted()(and optionalblindIndex). - For nested objects, use
@Type(() => ChildDto)fromclass-transformeron the parent property. - On the route,
@EncryptedRoute({ bodyDto, responseDto }). - Register
DecryptionInterceptorand/orEncryptionInterceptor.
import { Controller, Post, Body, UseInterceptors } from '@nestjs/common';
import { Type } from 'class-transformer';
import {
Encrypted,
EncryptedRoute,
DecryptionInterceptor,
EncryptionInterceptor,
} from 'cryptonest';
class ChildDto {
@Encrypted()
note!: string;
}
class CreateUserDto {
@Encrypted()
phone!: string;
@Type(() => ChildDto)
meta!: ChildDto;
}
@Controller('users')
@UseInterceptors(DecryptionInterceptor, EncryptionInterceptor)
export class UsersController {
@Post()
@EncryptedRoute({ bodyDto: CreateUserDto, responseDto: CreateUserDto })
create(@Body() body: CreateUserDto) {
return body;
}
}For tests or custom flows: applyEncryptedFieldTransforms(encryption, DtoClass, plain, 'toClass' | 'toPlain') (async).
| Driver | Prefix / shape |
|---|---|
| Local | v1: + b64url key id, iv, ciphertext, tag |
| AWS KMS | v1: + key id, wrapped DEK, iv, ciphertext, tag |
| GCP KMS | v2:gcp-kms: + … |
| Vault Transit | v2:vault-transit: + … |
| Hybrid RSA | v2:rsa-oaep-256: + … |
Details: src/lib/envelope.ts (serialize* / parseEnvelope).
cryptonest: module, service, interceptors, envelope helpers, decorators.cryptonest/aws-kms,cryptonest/gcp-kms,cryptonest/vault-transit,cryptonest/hybrid-rsa: driver entry points (optional SDK loading).
- Use ≥ 32-byte secrets for AES keys and for
blindIndexSecret. - Keep keys in a secret manager; never commit them.
- Put the current key first in
keys; older keys are decrypt-only during rotation. - With KMS, plaintext still exists in application memory during encrypt/decrypt; use least-privilege on CMK / service accounts.
- Envelope drivers validate that the ciphertext key id segment matches the configured logical key where applicable (AWS, GCP, Vault).
MIT