Signed isn't secret
The signature proves who sent a request, but anyone on the path can still read it. AES-GCM encrypts the body *and* adds an authentication tag, so the ciphertext is unreadable without the key and any change makes decryption fail. One rule matters above all: never reuse an IV with the same key.
Encrypt and decrypt
async function aesKey(hexKey) {
return crypto.subtle.importKey("raw", fromHex(hexKey), "AES-GCM", false, ["encrypt", "decrypt"]);
}
async function encrypt(hexKey, text, ivHex) {
const iv = ivHex ? fromHex(ivHex) : crypto.getRandomValues(new Uint8Array(12)); // never reuse an IV
const data = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, await aesKey(hexKey), enc.encode(text));
return `${toHex(iv)}:${toHex(data)}`;
}
async function decrypt(hexKey, payload) {
const [ivHex, dataHex] = String(payload).split(":");
try {
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: fromHex(ivHex) }, await aesKey(hexKey), fromHex(dataHex));
return new TextDecoder().decode(plain);
} catch {
throw new Error("Decryption failed"); // wrong key, or tampered: GCM's tag doesn't match
}
}