|
3 | 3 | // SPDX-License-Identifier: Apache-2.0 |
4 | 4 |
|
5 | 5 | import { x25519 } from "@noble/curves/ed25519" |
6 | | -import crypto from 'crypto' |
7 | 6 |
|
8 | | -// Convert hex string to Uint8Array |
9 | | -function hexToUint8Array(hex: string) { |
10 | | - hex = hex.startsWith("0x") ? hex.slice(2) : hex; |
| 7 | +function hexToUint8Array(hex: string): Uint8Array { |
| 8 | + hex = hex.startsWith("0x") ? hex.slice(2) : hex |
11 | 9 | return new Uint8Array( |
12 | | - hex.match(/.{1,2}/g)?.map((byte: string) => parseInt(byte, 16)) ?? [], |
13 | | - ); |
| 10 | + hex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) ?? [], |
| 11 | + ) |
14 | 12 | } |
15 | 13 |
|
16 | | -function uint8ArrayToHex(buffer: Uint8Array) { |
| 14 | +function uint8ArrayToHex(buffer: Uint8Array): string { |
17 | 15 | return Array.from(buffer) |
18 | | - .map((byte: number) => byte.toString(16).padStart(2, "0")) |
19 | | - .join(""); |
| 16 | + .map((byte) => byte.toString(16).padStart(2, "0")) |
| 17 | + .join("") |
20 | 18 | } |
21 | 19 |
|
22 | 20 | export interface EnvVar { |
23 | 21 | key: string |
24 | 22 | value: string |
25 | 23 | } |
26 | 24 |
|
27 | | -// Encrypt environment variables |
28 | | -export async function encryptEnvVars(envs: EnvVar[], publicKeyHex: string) { |
29 | | - // Prepare environment data |
30 | | - const envsJson = JSON.stringify({ env: envs }); |
| 25 | +/** |
| 26 | + * ECIES-encrypt a set of environment variables against a recipient's x25519 |
| 27 | + * public key. Works on Node 18+ and modern browsers — uses `globalThis.crypto` |
| 28 | + * (Web Crypto API) and @noble/curves. |
| 29 | + */ |
| 30 | +export async function encryptEnvVars( |
| 31 | + envs: EnvVar[], |
| 32 | + publicKeyHex: string, |
| 33 | +): Promise<string> { |
| 34 | + const envsJson = JSON.stringify({ env: envs }) |
31 | 35 |
|
32 | | - // Generate private key and derive public key |
33 | | - const privateKey = x25519.utils.randomPrivateKey(); |
34 | | - const publicKey = x25519.getPublicKey(privateKey); |
| 36 | + const privateKey = x25519.utils.randomPrivateKey() |
| 37 | + const publicKey = x25519.getPublicKey(privateKey) |
35 | 38 |
|
36 | | - // Generate shared key |
37 | | - const remotePubkey = hexToUint8Array(publicKeyHex); |
38 | | - const shared = x25519.getSharedSecret(privateKey, remotePubkey); |
| 39 | + const remotePubkey = hexToUint8Array(publicKeyHex) |
| 40 | + const shared = x25519.getSharedSecret(privateKey, remotePubkey) |
39 | 41 |
|
40 | | - // Import shared key for AES-GCM |
41 | 42 | const importedShared = await crypto.subtle.importKey( |
42 | 43 | "raw", |
43 | 44 | new Uint8Array(shared), |
44 | 45 | { name: "AES-GCM", length: 256 }, |
45 | 46 | true, |
46 | 47 | ["encrypt"], |
47 | | - ); |
| 48 | + ) |
48 | 49 |
|
49 | | - // Encrypt the data |
50 | | - const iv = crypto.getRandomValues(new Uint8Array(12)); |
| 50 | + const iv = crypto.getRandomValues(new Uint8Array(12)) |
51 | 51 | const encrypted = await crypto.subtle.encrypt( |
52 | 52 | { name: "AES-GCM", iv }, |
53 | 53 | importedShared, |
54 | 54 | new TextEncoder().encode(envsJson), |
55 | | - ); |
| 55 | + ) |
56 | 56 |
|
57 | | - // Combine all components |
58 | 57 | const result = new Uint8Array( |
59 | 58 | publicKey.length + iv.length + encrypted.byteLength, |
60 | | - ); |
| 59 | + ) |
| 60 | + result.set(publicKey) |
| 61 | + result.set(iv, publicKey.length) |
| 62 | + result.set(new Uint8Array(encrypted), publicKey.length + iv.length) |
61 | 63 |
|
62 | | - result.set(publicKey); |
63 | | - result.set(iv, publicKey.length); |
64 | | - result.set(new Uint8Array(encrypted), publicKey.length + iv.length); |
65 | | - |
66 | | - return uint8ArrayToHex(result); |
| 64 | + return uint8ArrayToHex(result) |
67 | 65 | } |
0 commit comments