Skip to content

Instantly share code, notes, and snippets.

@RhysSullivan
Created February 19, 2024 23:15
Show Gist options
  • Save RhysSullivan/2e5eca8fb047a1d50ba85f527c380bef to your computer and use it in GitHub Desktop.
Save RhysSullivan/2e5eca8fb047a1d50ba85f527c380bef to your computer and use it in GitHub Desktop.
let __next_loaded_action_key: CryptoKey;
export async function getActionEncryptionKey() {
if (__next_loaded_action_key) {
return __next_loaded_action_key;
}
const rawKey = process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY;
if (rawKey === undefined) {
throw new Error("Missing encryption key for Server Actions");
}
__next_loaded_action_key = await crypto.subtle.importKey(
"raw",
stringToUint8Array(atob(rawKey)),
"AES-GCM",
true,
["encrypt", "decrypt"]
);
return __next_loaded_action_key;
}
const textDecoder = new TextDecoder();
export function stringToUint8Array(binary: string) {
const len = binary.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binary.charCodeAt(i);
}
return arr;
}
export function encrypt(key: CryptoKey, iv: Uint8Array, data: Uint8Array) {
return crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
},
key,
data
);
}
export function decrypt(key: CryptoKey, iv: Uint8Array, data: Uint8Array) {
return crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
},
key,
data
);
}
async function decodeActionBoundArg(actionId: string, arg: string) {
const key = await getActionEncryptionKey();
if (typeof key === "undefined") {
throw new Error(
`Missing encryption key for Server Action. This is a bug in Next.js`
);
}
// Get the iv (16 bytes) and the payload from the arg.
const originalPayload = atob(arg);
const ivValue = originalPayload.slice(0, 16);
const payload = originalPayload.slice(16);
if (payload === undefined) {
throw new Error("Invalid Server Action payload.");
}
const decrypted = textDecoder.decode(
await decrypt(key, stringToUint8Array(ivValue), stringToUint8Array(payload))
);
if (!decrypted.startsWith(actionId)) {
throw new Error("Invalid Server Action payload: failed to decrypt.");
}
return decrypted.slice(actionId.length);
}
function arrayBufferToString(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
// @anonrig: V8 has a limit of 65535 arguments in a function.
// For len < 65535, this is faster.
// https://github.com/vercel/next.js/pull/56377#pullrequestreview-1656181623
if (len < 65535) {
return String.fromCharCode.apply(null, bytes as unknown as number[]);
}
let binary = "";
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return binary;
}
async function generateRandomActionKeyRaw(dev?: boolean) {
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
const exported = await crypto.subtle.exportKey("raw", key);
const b64 = btoa(arrayBufferToString(exported));
__next_loaded_action_key = key;
return b64;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment