the universal sandbox runtime for agents and humans.
pocketenv.io
sandbox
openclaw
agent
claude-code
vercel-sandbox
deno-sandbox
cloudflare-sandbox
atproto
sprites
daytona
1import sodium from "libsodium-wrappers";
2
3export type SSHKeyPair = {
4 publicKey: string;
5 privateKey: string;
6 seedBase64: string;
7};
8
9function u32(n: number): Uint8Array {
10 return new Uint8Array([
11 (n >>> 24) & 0xff,
12 (n >>> 16) & 0xff,
13 (n >>> 8) & 0xff,
14 n & 0xff,
15 ]);
16}
17
18function concatBytes(...arrays: Uint8Array[]): Uint8Array {
19 const total = arrays.reduce((sum, arr) => sum + arr.length, 0);
20 const out = new Uint8Array(total);
21 let offset = 0;
22 for (const arr of arrays) {
23 out.set(arr, offset);
24 offset += arr.length;
25 }
26 return out;
27}
28
29function sshString(bytes: Uint8Array): Uint8Array {
30 return concatBytes(u32(bytes.length), bytes);
31}
32
33function text(value: string): Uint8Array {
34 return new TextEncoder().encode(value);
35}
36
37function wrapPem(label: string, bytes: Uint8Array): string {
38 const base64 = sodium.to_base64(bytes, sodium.base64_variants.ORIGINAL);
39 const lines = base64.match(/.{1,70}/g)?.join("\n") ?? base64;
40 return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----\n`;
41}
42
43function buildEd25519PublicKeyBlob(publicKey: Uint8Array): Uint8Array {
44 return concatBytes(sshString(text("ssh-ed25519")), sshString(publicKey));
45}
46
47function publicLineFromPublicKey(
48 publicKey: Uint8Array,
49 comment: string,
50): string {
51 const blob = buildEd25519PublicKeyBlob(publicKey);
52 return `ssh-ed25519 ${sodium.to_base64(blob, sodium.base64_variants.ORIGINAL)} ${comment}`;
53}
54
55function buildOpenSSHEd25519PrivateKey(
56 publicKey: Uint8Array,
57 seed: Uint8Array,
58 comment: string,
59): string {
60 if (publicKey.length !== 32) throw new Error("Invalid public key length");
61 if (seed.length !== 32) throw new Error("Invalid seed length");
62
63 const privateKey64 = concatBytes(seed, publicKey);
64 const publicBlob = buildEd25519PublicKeyBlob(publicKey);
65 const checkint = crypto.getRandomValues(new Uint32Array(1))[0]!;
66 const commentBytes = text(comment);
67
68 const privateSectionWithoutPadding = concatBytes(
69 u32(checkint),
70 u32(checkint),
71 sshString(text("ssh-ed25519")),
72 sshString(publicKey),
73 sshString(privateKey64),
74 sshString(commentBytes),
75 );
76
77 const blockSize = 8;
78 const remainder = privateSectionWithoutPadding.length % blockSize;
79 const padLen = remainder === 0 ? 0 : blockSize - remainder;
80
81 const padding = new Uint8Array(padLen);
82 for (let i = 0; i < padLen; i++) padding[i] = i + 1;
83
84 const privateSection = concatBytes(privateSectionWithoutPadding, padding);
85
86 const opensshKey = concatBytes(
87 text("openssh-key-v1\0"),
88 sshString(text("none")),
89 sshString(text("none")),
90 sshString(new Uint8Array()),
91 u32(1),
92 sshString(publicBlob),
93 sshString(privateSection),
94 );
95
96 return wrapPem("OPENSSH PRIVATE KEY", opensshKey);
97}
98
99export async function generateEd25519SSHKeyPair(
100 comment = "user@browser",
101): Promise<SSHKeyPair> {
102 await sodium.ready;
103
104 const seed = new Uint8Array(32);
105 crypto.getRandomValues(seed);
106
107 const kp = sodium.crypto_sign_seed_keypair(seed);
108 const publicKey = new Uint8Array(kp.publicKey);
109
110 const publicKeyOpenSSH = publicLineFromPublicKey(publicKey, comment);
111 const privateKeyOpenSSH = buildOpenSSHEd25519PrivateKey(
112 publicKey,
113 seed,
114 comment,
115 );
116
117 return {
118 publicKey: publicKeyOpenSSH,
119 privateKey: privateKeyOpenSSH,
120 seedBase64: sodium.to_base64(seed, sodium.base64_variants.ORIGINAL),
121 };
122}