| | |
| | |
| | import path from 'path' |
| | import fs from 'fs' |
| | import { getStorageDirectory } from '../cache-dir' |
| | import { arrayBufferToString } from './encryption-utils' |
| |
|
| | |
| | |
| | let __next_encryption_key_generation_promise: Promise<string> | null = null |
| | const CONFIG_FILE = '.rscinfo' |
| | const ENCRYPTION_KEY = 'encryption.key' |
| | const ENCRYPTION_EXPIRE_AT = 'encryption.expire_at' |
| | const EXPIRATION = 1000 * 60 * 60 * 24 * 14 |
| |
|
| | async function writeCache(distDir: string, configValue: string) { |
| | const cacheBaseDir = getStorageDirectory(distDir) |
| | if (!cacheBaseDir) return |
| |
|
| | const configPath = path.join(cacheBaseDir, CONFIG_FILE) |
| | if (!fs.existsSync(cacheBaseDir)) { |
| | await fs.promises.mkdir(cacheBaseDir, { recursive: true }) |
| | } |
| | await fs.promises.writeFile( |
| | configPath, |
| | JSON.stringify({ |
| | [ENCRYPTION_KEY]: configValue, |
| | [ENCRYPTION_EXPIRE_AT]: Date.now() + EXPIRATION, |
| | }) |
| | ) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async function loadOrGenerateKey( |
| | distDir: string, |
| | isBuild: boolean, |
| | generateKey: () => Promise<string> |
| | ): Promise<string> { |
| | const cacheBaseDir = getStorageDirectory(distDir) |
| |
|
| | if (!cacheBaseDir) { |
| | |
| | |
| | return await generateKey() |
| | } |
| |
|
| | const configPath = path.join(cacheBaseDir, CONFIG_FILE) |
| | async function hasCachedKey(): Promise<false | string> { |
| | if (!fs.existsSync(configPath)) return false |
| | try { |
| | const config = JSON.parse(await fs.promises.readFile(configPath, 'utf8')) |
| | if (!config) return false |
| | if ( |
| | typeof config[ENCRYPTION_KEY] !== 'string' || |
| | typeof config[ENCRYPTION_EXPIRE_AT] !== 'number' |
| | ) { |
| | return false |
| | } |
| | |
| | |
| | |
| | if (isBuild && config[ENCRYPTION_EXPIRE_AT] < Date.now()) { |
| | return false |
| | } |
| | const cachedKey = config[ENCRYPTION_KEY] |
| |
|
| | |
| | |
| | if ( |
| | cachedKey && |
| | process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY && |
| | cachedKey !== process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY |
| | ) { |
| | return false |
| | } |
| | return cachedKey |
| | } catch { |
| | |
| | return false |
| | } |
| | } |
| | const maybeValidKey = await hasCachedKey() |
| | if (typeof maybeValidKey === 'string') { |
| | return maybeValidKey |
| | } |
| | const key = await generateKey() |
| | await writeCache(distDir, key) |
| |
|
| | return key |
| | } |
| |
|
| | export async function generateEncryptionKeyBase64({ |
| | isBuild, |
| | distDir, |
| | }: { |
| | isBuild: boolean |
| | distDir: string |
| | }) { |
| | |
| | if (!__next_encryption_key_generation_promise) { |
| | __next_encryption_key_generation_promise = loadOrGenerateKey( |
| | distDir, |
| | isBuild, |
| | async () => { |
| | const providedKey = process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY |
| |
|
| | if (providedKey) { |
| | return providedKey |
| | } |
| | const key = await crypto.subtle.generateKey( |
| | { |
| | name: 'AES-GCM', |
| | length: 256, |
| | }, |
| | true, |
| | ['encrypt', 'decrypt'] |
| | ) |
| | const exported = await crypto.subtle.exportKey('raw', key) |
| | return btoa(arrayBufferToString(exported)) |
| | } |
| | ) |
| | } |
| | return __next_encryption_key_generation_promise |
| | } |
| |
|