DynamoDB
Encrypt DynamoDB attributes with @cipherstash/stack, including bulk operations and equality lookups over HMAC attributes.
CipherStash's DynamoDB helper encrypts and decrypts items without wrapping the
AWS SDK. Your application keeps control of every PutCommand, GetCommand,
and QueryCommand; encryptedDynamoDB transforms the item immediately before
a write and after a read.
DynamoDB does not use EQL. Equality-enabled attributes store an HMAC search term alongside the ciphertext so they can participate in key conditions.
Install
npm install @cipherstash/stack @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodbDefine the encrypted attributes
Use the v3 schema builder. TextEq produces an equality term; Text is
storage-only.
import { Encryption } from "@cipherstash/stack"
import { encryptedDynamoDB } from "@cipherstash/stack/dynamodb"
import { encryptedTable, types } from "@cipherstash/stack/v3"
export const users = encryptedTable("users", {
email: types.TextEq("email"),
name: types.Text("name"),
})
export const encryptionClient = await Encryption({ schemas: [users] })
export const dynamo = encryptedDynamoDB({ encryptionClient })For an equality-enabled email, the encrypted item contains:
| Attribute | Purpose |
|---|---|
email__source | Ciphertext used for decryption |
email__hmac | Equality term used in a partition key, sort key, or GSI |
Non-encrypted attributes pass through unchanged. Ordering and free-text terms have no DynamoDB query surface and are not stored.
Encrypt and write an item
import { PutCommand } from "@aws-sdk/lib-dynamodb"
import { docClient } from "./aws"
import { dynamo, users } from "./encryption"
const encrypted = await dynamo.encryptModel(
{ pk: "user#1", email: "[email protected]", name: "Alice", role: "admin" },
users,
)
if (encrypted.failure) throw encrypted.failure
await docClient.send(new PutCommand({
TableName: "Users",
Item: encrypted.data,
}))The result retains pk and role, replaces email with email__source and
email__hmac, and replaces name with name__source.
Read and decrypt
import { GetCommand } from "@aws-sdk/lib-dynamodb"
const stored = await docClient.send(new GetCommand({
TableName: "Users",
Key: { pk: "user#1" },
}))
const decrypted = await dynamo.decryptModel(stored.Item ?? {}, users)
if (decrypted.failure) throw decrypted.failure
console.log(decrypted.data.email)Use bulkEncryptModels and bulkDecryptModels before BatchWriteCommand and
after BatchGetCommand to process many items in one ZeroKMS round trip.
Query an equality-enabled attribute
Encrypt the operand with the same table and column definition, then use its HMAC as the DynamoDB key value:
import { QueryCommand } from "@aws-sdk/lib-dynamodb"
const term = await encryptionClient.encryptQuery("[email protected]", {
table: users,
column: users.email,
})
if (term.failure) throw term.failure
const result = await docClient.send(new QueryCommand({
TableName: "Users",
IndexName: "EmailIndex",
KeyConditionExpression: "email__hmac = :email",
ExpressionAttributeValues: { ":email": term.data.hm },
}))
const decrypted = await dynamo.bulkDecryptModels(result.Items ?? [], users)
if (decrypted.failure) throw decrypted.failureAn encrypted HMAC attribute can back a table key or GSI. Ciphertext source
attributes do not support ranges, begins_with, or contains.
Read items written by EQL v2
Keep the current v3 table declaration and opt into legacy decoding while old items remain:
const decrypted = await dynamo.decryptModel(storedItem, users, {
storedEqlVersion: 2,
})New writes always use the current format. Remove the option after every legacy item has been rewritten.
For every method, option, and return type, see the generated DynamoDB API reference.