Files
bare-operating-system/packages/bare-os-booter/lib/ssh-keygen-cli.js
T
Raven Scott 27292f2ba6 Network/auth/delegate hardening
hdms
Added ls alias to list.
Made delegate failures explicit and nonzero in packages/bare-os-coreutils/src/hdms.js.
Added nonzero error exit in packages/bare-os-booter/lib/hdms-manager.js.
git-pear
Implemented clone subcommand routing to git clone in packages/bare-os-coreutils/src/git-pear.js.
trustctl
Added ls alias to status/policy output in packages/bare-os-coreutils/src/trustctl.js.
oidc-publish
Added explicit unknown-subcommand handling and publish subcommand compatibility in packages/bare-os-coreutils/src/oidc-publish.js.
ssh-keygen
Wrapped delegate invocation with explicit error propagation in packages/bare-os-coreutils/src/ssh-keygen.js.
Added success output on generated keypair in packages/bare-os-booter/lib/ssh-keygen-cli.js.
sshd
Added -t config test mode and explicit exit semantics in packages/bare-os-booter/lib/bare-openssh.js.
Ensured wrapper sets exit code consistently in packages/bare-os-openssh/src/sshd.js.
telnet
Changed connector preference to use net.createConnection first when available, then syscall bridge fallback, in packages/bare-os-coreutils/src/telnet.js.
crontab -e flow

Implemented edit flow with VISUAL/EDITOR fallback, unlocked-state checks, temp file handling, install, and cleanup in packages/bare-os-coreutils/src/crontab.js.
Shell/runtime hardcore semantics

Added numeric brace range expansion {1..5} in packages/bare-os-booter/lib/shell-glob.js.
Enabled brace expansion by default unless explicitly disabled.
Added normalization for inline brace-expression tokens in packages/bare-os-booter/lib/shell.js.
Added arithmetic command-form handling for (( ... )) in packages/bare-os-booter/lib/shell.js.
Extended shell signal trap dispatch support for USR1/USR2 (in addition to INT/TERM) in packages/bare-os-booter/index.js.
Hardened kill command delivery validation in packages/bare-os-coreutils/src/kill.js.
Regression tests

Added new: packages/bare-os-coreutils/test/hardcore-bugs.test.mjs.
Extended shell tests in packages/bare-os-booter/test.js for:
default cmdsub behavior,
brace range expansion,
arithmetic command form.
Existing regression files still pass after updates.
2026-04-27 08:54:50 -04:00

168 lines
4.6 KiB
JavaScript

/**
* 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<string, unknown>} 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<string, unknown>} */
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))
ctx.console.log('generated keypair: ' + keyPath + ' and ' + keyPath + '.pub')
} catch (e) {
ctx.console.error(
'ssh-keygen: ' + ((e && /** @type {Error} */ (e).message) || String(e))
)
ctx.exitCode = 1
}
}