- Add createBareOsDiskOsBridge for Hyperdrive searchLocal + whitelisted bare_os.* execRpc; wire disk.os after initd - Expose ctx.bareOsRunSystemctlCli; route /bin/systemctl and journalctl through it (sync seeder kernel/bin) - Implement ssh-keygen passphrase envelope (bareOsKeySchema 2, PBKDF2 + ChaCha20-Poly1305); add warc and archive delegates - Extend basenc (--base32/--base64); improve hostid/users session UX - Introduce bare-os-boot-phases, bare-os-errors; CI boot-step alignment - Bump BARE_OS_CTX_API_VERSION to 1.28.0; update ctx d.ts and verifiers - Add KERNEL_CONTRACT, OTA_AND_BUNDLES, PLACEHOLDER_BASELINE; handbook disk.os + security hooks; reference docs and CHANGELOG
167 lines
4.6 KiB
JavaScript
167 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))
|
|
} catch (e) {
|
|
ctx.console.error(
|
|
'ssh-keygen: ' + ((e && /** @type {Error} */ (e).message) || String(e))
|
|
)
|
|
ctx.exitCode = 1
|
|
}
|
|
}
|