@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
158 lines (143 loc) • 4.55 kB
text/typescript
/**
* Temporal Payload Encryption Codec
*
* Encrypts workflow payloads using AES-256-GCM following Temporal's
* recommended pattern: serialize the entire payload with Protobuf,
* then encrypt.
*
* Reference: https://github.com/temporalio/samples-typescript/blob/main/encryption/src/encryption-codec.ts
*/
import { webcrypto as crypto } from "node:crypto";
import {
METADATA_ENCODING_KEY,
Payload,
PayloadCodec,
ValueError,
} from "@temporalio/common";
import proto from "@temporalio/proto";
import { logger } from "@mesh-tech/logger";
const { temporal } = proto;
const ENCODING = "binary/encrypted";
const METADATA_ENCRYPTION_KEY_ID = "encryption-key-id";
const IV_LENGTH = 12;
/**
* Derive a CryptoKey from an encoding key string using SHA-256
*/
async function deriveKey(encodingKey: string): Promise<crypto.CryptoKey> {
const keyData = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(encodingKey)
);
return crypto.subtle.importKey("raw", keyData, { name: "AES-GCM" }, false, [
"encrypt",
"decrypt",
]);
}
/**
* Encrypt data using AES-256-GCM
* Output: IV (12 bytes) + Ciphertext+AuthTag (WebCrypto format)
*/
async function encrypt(
data: Uint8Array,
key: crypto.CryptoKey
): Promise<Uint8Array> {
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
data
);
// Prepend IV to ciphertext+authTag
const result = new Uint8Array(IV_LENGTH + encrypted.byteLength);
result.set(iv, 0);
result.set(new Uint8Array(encrypted), IV_LENGTH);
return result;
}
/**
* Decrypt data using AES-256-GCM
* Input: IV (12 bytes) + Ciphertext+AuthTag
*/
async function decrypt(
data: Uint8Array,
key: crypto.CryptoKey
): Promise<Uint8Array> {
const iv = data.slice(0, IV_LENGTH);
const ciphertextWithTag = data.slice(IV_LENGTH);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
ciphertextWithTag
);
return new Uint8Array(decrypted);
}
/**
* Payload codec that encrypts/decrypts payloads using AES-256-GCM.
*
* Uses Protobuf serialization for the entire payload (data + metadata)
* following Temporal's recommended pattern.
*/
export class EncryptionCodec implements PayloadCodec {
private constructor(
private readonly key: crypto.CryptoKey,
private readonly keyId: string
) {}
static async create(encodingKey: string): Promise<EncryptionCodec> {
const key = await deriveKey(encodingKey);
// Use truncated hash as key ID for identification
const keyIdData = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(encodingKey)
);
const keyId = Buffer.from(keyIdData).toString("hex").slice(0, 16);
return new EncryptionCodec(key, keyId);
}
async encode(payloads: Payload[]): Promise<Payload[]> {
return Promise.all(
payloads.map(async (payload) => ({
metadata: {
[METADATA_ENCODING_KEY]: new TextEncoder().encode(ENCODING),
[METADATA_ENCRYPTION_KEY_ID]: new TextEncoder().encode(this.keyId),
},
// Serialize entire payload with Protobuf, then encrypt
data: await encrypt(
temporal.api.common.v1.Payload.encode(payload).finish(),
this.key
),
}))
);
}
async decode(payloads: Payload[]): Promise<Payload[]> {
return Promise.all(
payloads.map(async (payload) => {
// Check if encrypted
const encoding = payload.metadata?.[METADATA_ENCODING_KEY];
if (!encoding) {
return payload;
}
const encodingStr = new TextDecoder().decode(encoding);
if (encodingStr !== ENCODING) {
return payload;
}
if (!payload.data) {
throw new ValueError("Payload data is missing");
}
// Decrypt and deserialize with Protobuf
const decryptedBytes = await decrypt(payload.data, this.key);
return temporal.api.common.v1.Payload.decode(decryptedBytes);
})
);
}
}
/**
* Creates an encryption codec from TEMPORAL_ENCODING_KEY env var.
* Returns undefined if not set.
*/
export async function createEncryptionCodec(): Promise<EncryptionCodec | undefined> {
const encodingKey = process.env.TEMPORAL_ENCODING_KEY;
if (!encodingKey) {
logger.warn("No TEMPORAL_ENCODING_KEY set, payloads will not be encrypted");
return undefined;
}
logger.info("Payload encryption enabled");
return EncryptionCodec.create(encodingKey);
}