@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
134 lines (112 loc) • 3.7 kB
text/typescript
/**
* Encryption Codec for Temporal Payload Encryption
*
* Implements AES-256-GCM encryption for workflow payloads.
* Uses TEMPORAL_ENCODING_KEY environment variable.
*
* This is a copy of the worker's encryption codec - both need
* to use the same encryption to communicate.
*/
import * as crypto from "crypto";
import {
PayloadCodec,
Payload,
encodingKeys,
METADATA_ENCODING_KEY,
} from "@temporalio/common";
import proto from "@temporalio/proto";
const { temporal } = proto;
const ENCODING_TYPE = "binary/encrypted";
const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;
/**
* Derive a 256-bit key from the encoding key using SHA-256
*/
async function deriveKey(encodingKey: string): Promise<Buffer> {
return crypto.createHash("sha256").update(encodingKey).digest();
}
/**
* Encrypt data using AES-256-GCM
*/
function encrypt(data: Uint8Array, key: Buffer): Uint8Array {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
const authTag = cipher.getAuthTag();
// Format: IV (12 bytes) + Ciphertext + AuthTag (16 bytes)
return Buffer.concat([iv, encrypted, authTag]);
}
/**
* Decrypt data using AES-256-GCM
*/
function decrypt(data: Uint8Array, key: Buffer): Uint8Array {
const iv = data.slice(0, IV_LENGTH);
const authTag = data.slice(-AUTH_TAG_LENGTH);
const ciphertext = data.slice(IV_LENGTH, -AUTH_TAG_LENGTH);
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}
/**
* Encryption codec implementing Temporal's PayloadCodec interface
*/
export class EncryptionCodec implements PayloadCodec {
private readonly key: Buffer;
private readonly keyId: string;
private constructor(key: Buffer, keyId: string) {
this.key = key;
this.keyId = keyId;
}
static async create(encodingKey: string): Promise<EncryptionCodec> {
const key = await deriveKey(encodingKey);
// Use first 8 chars of key hash as key ID
const keyId = crypto
.createHash("sha256")
.update(encodingKey)
.digest("hex")
.slice(0, 8);
return new EncryptionCodec(key, keyId);
}
async encode(payloads: Payload[]): Promise<Payload[]> {
return payloads.map((payload) => {
// Serialize the payload to bytes
const bytes = temporal.api.common.v1.Payload.encode(payload).finish();
// Encrypt
const encrypted = encrypt(bytes, this.key);
return {
metadata: {
[METADATA_ENCODING_KEY]: encodingKeys.METADATA_ENCODING_RAW,
"encryption-codec": Buffer.from(ENCODING_TYPE),
"encryption-key-id": Buffer.from(this.keyId),
},
data: encrypted,
};
});
}
async decode(payloads: Payload[]): Promise<Payload[]> {
return payloads.map((payload) => {
// Check if this payload is encrypted by us
const encoding = payload.metadata?.["encryption-codec"];
if (!encoding || Buffer.from(encoding).toString() !== ENCODING_TYPE) {
return payload;
}
// Decrypt
const decrypted = decrypt(payload.data!, this.key);
// Deserialize back to Payload
return temporal.api.common.v1.Payload.decode(decrypted);
});
}
}
/**
* Create encryption codec from environment variable
*/
export async function createEncryptionCodec(): Promise<
EncryptionCodec | undefined
> {
const encodingKey = process.env.TEMPORAL_ENCODING_KEY;
if (!encodingKey) {
return undefined;
}
return EncryptionCodec.create(encodingKey);
}