the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor SSH key helpers and export generator

Move SSH helper functions out of the hook and export
generateEd25519SSHKeyPair directly
Generate a 32-byte seed with crypto.getRandomValues and derive
keys via sodium.crypto_sign_seed_keypair
Construct the private secret as seed||publicKey, adjust OpenSSH
private-key padding (no extra full-block padding) and change PEM
base64 wrapping

+97 -103
+97 -103
apps/web/src/hooks/useSshKeys.ts
··· 6 6 seedBase64: string; 7 7 }; 8 8 9 - export const useSshKeys = () => { 10 - function u32(n: number): Uint8Array { 11 - return new Uint8Array([ 12 - (n >>> 24) & 0xff, 13 - (n >>> 16) & 0xff, 14 - (n >>> 8) & 0xff, 15 - n & 0xff, 16 - ]); 9 + function 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 + 18 + function 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 + 23 + for (const arr of arrays) { 24 + out.set(arr, offset); 25 + offset += arr.length; 17 26 } 18 27 19 - function concatBytes(...arrays: Uint8Array[]): Uint8Array { 20 - const total = arrays.reduce((sum, arr) => sum + arr.length, 0); 21 - const out = new Uint8Array(total); 22 - let offset = 0; 28 + return out; 29 + } 23 30 24 - for (const arr of arrays) { 25 - out.set(arr, offset); 26 - offset += arr.length; 27 - } 31 + function sshString(bytes: Uint8Array): Uint8Array { 32 + return concatBytes(u32(bytes.length), bytes); 33 + } 28 34 29 - return out; 30 - } 35 + function text(value: string): Uint8Array { 36 + return new TextEncoder().encode(value); 37 + } 31 38 32 - function sshString(bytes: Uint8Array): Uint8Array { 33 - return concatBytes(u32(bytes.length), bytes); 34 - } 39 + function randomU32(): number { 40 + const buf = new Uint32Array(1); 41 + crypto.getRandomValues(buf); 42 + return buf[0]!; 43 + } 35 44 36 - function text(value: string): Uint8Array { 37 - return new TextEncoder().encode(value); 38 - } 45 + function wrapPem(label: string, bytes: Uint8Array): string { 46 + const base64 = btoa(String.fromCharCode(...bytes)); 47 + const lines = base64.match(/.{1,70}/g)?.join("\n") ?? base64; 48 + return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----`; 49 + } 39 50 40 - function randomU32(): number { 41 - const buf = new Uint32Array(1); 42 - crypto.getRandomValues(buf); 43 - return buf[0]!; 44 - } 51 + function buildEd25519PublicKeyBlob(publicKey: Uint8Array): Uint8Array { 52 + return concatBytes(sshString(text("ssh-ed25519")), sshString(publicKey)); 53 + } 45 54 46 - function wrapPem(label: string, bytes: Uint8Array): string { 47 - const base64 = sodium.to_base64(bytes, sodium.base64_variants.ORIGINAL); 48 - const lines = base64.match(/.{1,64}/g)?.join("\n") ?? base64; 49 - return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----`; 55 + function buildOpenSSHEd25519PrivateKey( 56 + publicKey: Uint8Array, 57 + secretKey64: Uint8Array, // must be seed(32) || publicKey(32) 58 + comment: string, 59 + ): string { 60 + if (publicKey.length !== 32) { 61 + throw new Error("Invalid Ed25519 public key length"); 50 62 } 51 63 52 - function buildEd25519PublicKeyBlob(publicKey: Uint8Array): Uint8Array { 53 - return concatBytes(sshString(text("ssh-ed25519")), sshString(publicKey)); 64 + if (secretKey64.length !== 64) { 65 + throw new Error("Invalid Ed25519 secret key length"); 54 66 } 55 67 56 - function buildOpenSSHEd25519PrivateKey( 57 - publicKey: Uint8Array, 58 - secretKey: Uint8Array, 59 - comment: string, 60 - ): string { 61 - // libsodium Ed25519 secretKey is 64 bytes: 62 - // first 32 = seed, last 32 = public key 63 - if (publicKey.length !== 32) { 64 - throw new Error("Invalid Ed25519 public key length"); 65 - } 66 - 67 - if (secretKey.length !== 64) { 68 - throw new Error("Invalid Ed25519 secret key length"); 69 - } 70 - 71 - const publicBlob = buildEd25519PublicKeyBlob(publicKey); 68 + const publicBlob = buildEd25519PublicKeyBlob(publicKey); 69 + const checkint = randomU32(); 70 + const commentBytes = text(comment); 72 71 73 - const checkint = randomU32(); 74 - const commentBytes = text(comment); 72 + const privateSectionWithoutPadding = concatBytes( 73 + u32(checkint), 74 + u32(checkint), 75 + sshString(text("ssh-ed25519")), 76 + sshString(publicKey), 77 + sshString(secretKey64), 78 + sshString(commentBytes), 79 + ); 75 80 76 - const privateSectionWithoutPadding = concatBytes( 77 - u32(checkint), 78 - u32(checkint), 79 - sshString(text("ssh-ed25519")), 80 - sshString(publicKey), 81 - sshString(secretKey), // 64 bytes: seed + public key 82 - sshString(commentBytes), 83 - ); 81 + const blockSize = 8; 82 + const remainder = privateSectionWithoutPadding.length % blockSize; 83 + const padLen = remainder === 0 ? 0 : blockSize - remainder; 84 84 85 - const blockSize = 8; 86 - const remainder = privateSectionWithoutPadding.length % blockSize; 87 - const padLen = remainder === 0 ? blockSize : blockSize - remainder; 85 + const padding = new Uint8Array(padLen); 86 + for (let i = 0; i < padLen; i++) { 87 + padding[i] = i + 1; 88 + } 88 89 89 - const padding = new Uint8Array(padLen); 90 - for (let i = 0; i < padLen; i++) { 91 - padding[i] = i + 1; 92 - } 90 + const privateSection = concatBytes(privateSectionWithoutPadding, padding); 93 91 94 - const privateSection = concatBytes(privateSectionWithoutPadding, padding); 92 + const opensshKey = concatBytes( 93 + text("openssh-key-v1\0"), 94 + sshString(text("none")), // ciphername 95 + sshString(text("none")), // kdfname 96 + sshString(new Uint8Array()), // kdfoptions 97 + u32(1), // number of keys 98 + sshString(publicBlob), // public key 99 + sshString(privateSection), // private section 100 + ); 95 101 96 - const opensshKey = concatBytes( 97 - text("openssh-key-v1\0"), 98 - sshString(text("none")), // ciphername 99 - sshString(text("none")), // kdfname 100 - sshString(new Uint8Array()), // kdfoptions 101 - u32(1), // number of keys 102 - sshString(publicBlob), // public key 103 - sshString(privateSection), // private section 104 - ); 102 + return wrapPem("OPENSSH PRIVATE KEY", opensshKey); 103 + } 105 104 106 - return wrapPem("OPENSSH PRIVATE KEY", opensshKey); 107 - } 105 + export async function generateEd25519SSHKeyPair( 106 + comment = "user@browser", 107 + ): Promise<SSHKeyPair> { 108 + await sodium.ready; 108 109 109 - async function generateEd25519SSHKeyPair( 110 - comment = "user@browser", 111 - ): Promise<SSHKeyPair> { 112 - await sodium.ready; 110 + const seed = new Uint8Array(32); 111 + crypto.getRandomValues(seed); 113 112 114 - const kp = sodium.crypto_sign_keypair(); 113 + const kp = sodium.crypto_sign_seed_keypair(seed); 115 114 116 - const publicKey = new Uint8Array(kp.publicKey); 117 - const secretKey = new Uint8Array(kp.privateKey); 118 - const seed = secretKey.slice(0, 32); 115 + const publicKey = new Uint8Array(kp.publicKey); 119 116 120 - const publicBlob = buildEd25519PublicKeyBlob(publicKey); 117 + const secretKey64 = concatBytes(seed, publicKey); 121 118 122 - const publicKeyOpenSSH = `ssh-ed25519 ${sodium.to_base64(publicBlob, sodium.base64_variants.ORIGINAL)} ${comment}`; 119 + const publicBlob = buildEd25519PublicKeyBlob(publicKey); 123 120 124 - const privateKeyOpenSSH = buildOpenSSHEd25519PrivateKey( 125 - publicKey, 126 - secretKey, 127 - comment, 128 - ); 121 + const publicKeyOpenSSH = `ssh-ed25519 ${sodium.to_base64(publicBlob, sodium.base64_variants.ORIGINAL)} ${comment}`; 129 122 130 - return { 131 - publicKey: publicKeyOpenSSH, 132 - privateKey: privateKeyOpenSSH, 133 - seedBase64: sodium.to_base64(seed, sodium.base64_variants.ORIGINAL), 134 - }; 135 - } 123 + const privateKeyOpenSSH = buildOpenSSHEd25519PrivateKey( 124 + publicKey, 125 + secretKey64, 126 + comment, 127 + ); 136 128 137 129 return { 138 - generateEd25519SSHKeyPair, 130 + publicKey: publicKeyOpenSSH, 131 + privateKey: privateKeyOpenSSH, 132 + seedBase64: sodium.to_base64(seed, sodium.base64_variants.ORIGINAL), 139 133 }; 140 - }; 134 + }