Orginize
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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 =
|
||||
typeof publicKey.export === 'function' ? publicKey.export() : publicKey._key
|
||||
const secRaw =
|
||||
typeof privateKey.export === 'function'
|
||||
? privateKey.export()
|
||||
: 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user