Created
February 19, 2024 23:15
-
-
Save RhysSullivan/2e5eca8fb047a1d50ba85f527c380bef to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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