/** * Ed25519 key generation for Bare OS using bare-crypto (not OpenSSH wire private format). * Writes OpenSSH-compatible `.pub` line + a Bare-OS JSON private envelope. * * Usage: ssh-keygen -t ed25519 -f PATH [-N pass] [-C comment] * Encrypted private keys use bareOsKeySchema 2 (PBKDF2-SHA256 + ChaCha20-Poly1305), aligned with account crypto profile. */ import bareCrypto from 'bare-crypto' import b4a from 'b4a' import { PBKDF2_ITERATIONS, sealBytes } from './identity-account.js' const { pbkdf2Sync, randomFillSync } = bareCrypto /** * @param {string} str */ function utf8Bytes(str) { return b4a.from(String(str), 'utf8') } /** * @param {Uint8Array} pub32 */ function sshEd25519PublicBlob(pub32) { const enc = (s) => { const b = typeof s === 'string' ? utf8Bytes(s) : s const out = new Uint8Array(4 + b.length) new DataView(out.buffer).setUint32(0, b.length, false) out.set(b, 4) return out } const a = enc('ssh-ed25519') const b = enc(pub32) const merged = new Uint8Array(a.length + b.length) merged.set(a, 0) merged.set(b, a.length) return merged } function b64(buf) { if (typeof Buffer !== 'undefined') return Buffer.from(buf).toString('base64') let bin = '' for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]) // eslint-disable-next-line no-undef return btoa(bin) } /** * @param {Record} ctx * @param {string[]} argv */ export async function runSshKeygenCli(ctx, argv) { const args = argv.slice(1) if ( args.includes('-h') || args.includes('--help') || args.includes('-?') || args.length === 0 ) { ctx.console.log( 'Usage: ssh-keygen -t ed25519 -f KEYFILE [-N passphrase] [-C comment]\n' + 'Bare OS: writes KEYFILE (JSON private envelope) and KEYFILE.pub (ssh-ed25519 line).\n' + 'Passphrase: schema 2 sealed key (PBKDF2 + ChaCha20-Poly1305); empty -N for plaintext schema 1.\n' ) return } let type = '' let keyPath = '' let pass = '' let comment = '' for (let i = 0; i < args.length; i++) { const a = args[i] if (a === '-t') type = String(args[++i] || '') else if (a === '-f') keyPath = String(args[++i] || '') else if (a === '-N') pass = String(args[++i] ?? '') else if (a === '-C') comment = String(args[++i] || '') else if (a.startsWith('-')) { ctx.console.error('ssh-keygen: unsupported option ' + a) ctx.exitCode = 1 return } } if (type !== 'ed25519') { ctx.console.error('ssh-keygen: only -t ed25519 is supported') ctx.exitCode = 1 return } if (!keyPath) { ctx.console.error('ssh-keygen: -f KEYFILE is required') ctx.exitCode = 1 return } const vfs = ctx.vfs const b4 = ctx.b4a if (!vfs || typeof vfs.resolveLogical !== 'function' || !b4) { ctx.console.error('ssh-keygen: vfs unavailable') ctx.exitCode = 1 return } const { publicKey, privateKey } = bareCrypto.generateKeyPair('ed25519') const pubRaw = /** @type {{ _key: Uint8Array }} */ (publicKey)._key const secRaw = /** @type {{ _key: Uint8Array }} */ (privateKey)._key const blob = sshEd25519PublicBlob(pubRaw) const pubLine = 'ssh-ed25519 ' + b64(blob) + ' ' + (comment || 'bare-os-ed25519') + '\n' /** @type {Record} */ let privEnvelope if (pass !== '') { const salt = new Uint8Array(16) randomFillSync(salt) const dk = pbkdf2Sync( utf8Bytes(pass), salt, PBKDF2_ITERATIONS, 32, 'sha256' ) const inner = JSON.stringify({ sk: b64(secRaw), pk: b64(pubRaw) }) const sealed = sealBytes(dk, utf8Bytes(inner)) privEnvelope = { bareOsKeySchema: 2, kty: 'ed25519', kdf: 'pbkdf2-sha256', aead: 'chacha20-poly1305', iterations: PBKDF2_ITERATIONS, salt: b64(salt), sealed: b64(sealed), comment: comment || 'bare-os-ed25519', atMs: Date.now() } } else { privEnvelope = { bareOsKeySchema: 1, kty: 'ed25519', sk: b64(secRaw), pk: b64(pubRaw), comment: comment || 'bare-os-ed25519', atMs: Date.now() } } const privJson = JSON.stringify(privEnvelope, null, 2) + '\n' const absKey = vfs.resolveLogical(keyPath) const absPub = vfs.resolveLogical(keyPath + '.pub') try { if (typeof vfs.writeFile !== 'function') { throw new Error('writeFile missing') } await vfs.writeFile(absKey, b4.from(privJson)) await vfs.writeFile(absPub, b4.from(pubLine)) } catch (e) { ctx.console.error( 'ssh-keygen: ' + ((e && /** @type {Error} */ (e).message) || String(e)) ) ctx.exitCode = 1 } }