UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

158 lines (143 loc) 4.55 kB
/** * 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); }