Files
bare-operating-system/packages/bare-os-openssh/vendor/bare-ssh2/lib/keygen.js
T
2026-04-08 22:21:22 -04:00

547 lines
18 KiB
JavaScript

'use strict'
require('./buffer-polyfill.js')
const {
createCipheriv,
generateKeyPair: generateKeyPair_,
generateKeyPairSync: generateKeyPairSync_,
getCurves,
randomBytes
} = require('../shims/crypto/index.js')
const { Ber } = require('asn1')
const bcrypt_pbkdf = require('bcrypt-pbkdf').pbkdf
const { CIPHER_INFO } = require('./protocol/crypto.js')
const SALT_LEN = 16
const DEFAULT_ROUNDS = 16
/** Bare `bare-crypto` / shims may omit `getCurves` (only needed for ECDSA keygen). */
const curves =
typeof getCurves === 'function'
? getCurves()
: ['prime256v1', 'secp384r1', 'secp521r1']
const ciphers = new Map(Object.entries(CIPHER_INFO))
function makeArgs(type, opts) {
if (typeof type !== 'string') throw new TypeError('Key type must be a string')
const publicKeyEncoding = { type: 'spki', format: 'der' }
const privateKeyEncoding = { type: 'pkcs8', format: 'der' }
switch (type.toLowerCase()) {
case 'rsa': {
if (typeof opts !== 'object' || opts === null)
throw new TypeError('Missing options object for RSA key')
const modulusLength = opts.bits
if (!Number.isInteger(modulusLength)) throw new TypeError('RSA bits must be an integer')
if (modulusLength <= 0 || modulusLength > 16384)
throw new RangeError('RSA bits must be non-zero and <= 16384')
return ['rsa', { modulusLength, publicKeyEncoding, privateKeyEncoding }]
}
case 'ecdsa': {
if (typeof opts !== 'object' || opts === null)
throw new TypeError('Missing options object for ECDSA key')
if (!Number.isInteger(opts.bits)) throw new TypeError('ECDSA bits must be an integer')
let namedCurve
switch (opts.bits) {
case 256:
namedCurve = 'prime256v1'
break
case 384:
namedCurve = 'secp384r1'
break
case 521:
namedCurve = 'secp521r1'
break
default:
throw new Error('ECDSA bits must be 256, 384, or 521')
}
if (!curves.includes(namedCurve)) throw new Error('Unsupported ECDSA bits value')
return ['ec', { namedCurve, publicKeyEncoding, privateKeyEncoding }]
}
case 'ed25519':
return ['ed25519', { publicKeyEncoding, privateKeyEncoding }]
default:
throw new Error(`Unsupported key type: ${type}`)
}
}
function parseDERs(keyType, pub, priv) {
switch (keyType) {
case 'rsa': {
// Note: we don't need to parse the public key since the PKCS8 private key
// already includes the public key parameters
// Parse private key
let reader = new Ber.Reader(priv)
reader.readSequence()
// - Version
if (reader.readInt() !== 0) throw new Error('Unsupported version in RSA private key')
// - Algorithm
reader.readSequence()
if (reader.readOID() !== '1.2.840.113549.1.1.1') throw new Error('Bad RSA private OID')
// - Algorithm parameters (RSA has none)
if (reader.readByte() !== Ber.Null)
throw new Error('Malformed RSA private key (expected null)')
if (reader.readByte() !== 0x00) {
throw new Error('Malformed RSA private key (expected zero-length null)')
}
reader = new Ber.Reader(reader.readString(Ber.OctetString, true))
reader.readSequence()
if (reader.readInt() !== 0) throw new Error('Unsupported version in RSA private key')
const n = reader.readString(Ber.Integer, true)
const e = reader.readString(Ber.Integer, true)
const d = reader.readString(Ber.Integer, true)
const p = reader.readString(Ber.Integer, true)
const q = reader.readString(Ber.Integer, true)
reader.readString(Ber.Integer, true) // dmp1
reader.readString(Ber.Integer, true) // dmq1
const iqmp = reader.readString(Ber.Integer, true)
/*
OpenSSH RSA private key:
string "ssh-rsa"
string n -- public
string e -- public
string d -- private
string iqmp -- private
string p -- private
string q -- private
*/
const keyName = Buffer.from('ssh-rsa')
const privBuf = Buffer.allocUnsafe(
4 +
keyName.length +
4 +
n.length +
4 +
e.length +
4 +
d.length +
4 +
iqmp.length +
4 +
p.length +
4 +
q.length
)
let pos = 0
privBuf.writeUInt32BE(keyName.length, (pos += 0))
privBuf.set(keyName, (pos += 4))
privBuf.writeUInt32BE(n.length, (pos += keyName.length))
privBuf.set(n, (pos += 4))
privBuf.writeUInt32BE(e.length, (pos += n.length))
privBuf.set(e, (pos += 4))
privBuf.writeUInt32BE(d.length, (pos += e.length))
privBuf.set(d, (pos += 4))
privBuf.writeUInt32BE(iqmp.length, (pos += d.length))
privBuf.set(iqmp, (pos += 4))
privBuf.writeUInt32BE(p.length, (pos += iqmp.length))
privBuf.set(p, (pos += 4))
privBuf.writeUInt32BE(q.length, (pos += p.length))
privBuf.set(q, (pos += 4))
/*
OpenSSH RSA public key:
string "ssh-rsa"
string e -- public
string n -- public
*/
const pubBuf = Buffer.allocUnsafe(4 + keyName.length + 4 + e.length + 4 + n.length)
pos = 0
pubBuf.writeUInt32BE(keyName.length, (pos += 0))
pubBuf.set(keyName, (pos += 4))
pubBuf.writeUInt32BE(e.length, (pos += keyName.length))
pubBuf.set(e, (pos += 4))
pubBuf.writeUInt32BE(n.length, (pos += e.length))
pubBuf.set(n, (pos += 4))
return { sshName: keyName.toString(), priv: privBuf, pub: pubBuf }
}
case 'ec': {
// Parse public key
let reader = new Ber.Reader(pub)
reader.readSequence()
reader.readSequence()
if (reader.readOID() !== '1.2.840.10045.2.1') throw new Error('Bad ECDSA public OID')
// Skip curve OID, we'll get it from the private key
reader.readOID()
let pubBin = reader.readString(Ber.BitString, true)
{
// Remove leading zero bytes
let i = 0
for (; i < pubBin.length && pubBin[i] === 0x00; ++i);
if (i > 0) pubBin = pubBin.slice(i)
}
// Parse private key
reader = new Ber.Reader(priv)
reader.readSequence()
// - Version
if (reader.readInt() !== 0) throw new Error('Unsupported version in ECDSA private key')
reader.readSequence()
if (reader.readOID() !== '1.2.840.10045.2.1') throw new Error('Bad ECDSA private OID')
const curveOID = reader.readOID()
let sshCurveName
switch (curveOID) {
case '1.2.840.10045.3.1.7':
// prime256v1/secp256r1
sshCurveName = 'nistp256'
break
case '1.3.132.0.34':
// secp384r1
sshCurveName = 'nistp384'
break
case '1.3.132.0.35':
// secp521r1
sshCurveName = 'nistp521'
break
default:
throw new Error('Unsupported curve in ECDSA private key')
}
reader = new Ber.Reader(reader.readString(Ber.OctetString, true))
reader.readSequence()
// - Version
if (reader.readInt() !== 1) throw new Error('Unsupported version in ECDSA private key')
// Add leading zero byte to prevent negative bignum in private key
const privBin = Buffer.concat([Buffer.from([0x00]), reader.readString(Ber.OctetString, true)])
/*
OpenSSH ECDSA private key:
string "ecdsa-sha2-<sshCurveName>"
string curve name
string Q -- public
string d -- private
*/
const keyName = Buffer.from(`ecdsa-sha2-${sshCurveName}`)
sshCurveName = Buffer.from(sshCurveName)
const privBuf = Buffer.allocUnsafe(
4 + keyName.length + 4 + sshCurveName.length + 4 + pubBin.length + 4 + privBin.length
)
let pos = 0
privBuf.writeUInt32BE(keyName.length, (pos += 0))
privBuf.set(keyName, (pos += 4))
privBuf.writeUInt32BE(sshCurveName.length, (pos += keyName.length))
privBuf.set(sshCurveName, (pos += 4))
privBuf.writeUInt32BE(pubBin.length, (pos += sshCurveName.length))
privBuf.set(pubBin, (pos += 4))
privBuf.writeUInt32BE(privBin.length, (pos += pubBin.length))
privBuf.set(privBin, (pos += 4))
/*
OpenSSH ECDSA public key:
string "ecdsa-sha2-<sshCurveName>"
string curve name
string Q -- public
*/
const pubBuf = Buffer.allocUnsafe(
4 + keyName.length + 4 + sshCurveName.length + 4 + pubBin.length
)
pos = 0
pubBuf.writeUInt32BE(keyName.length, (pos += 0))
pubBuf.set(keyName, (pos += 4))
pubBuf.writeUInt32BE(sshCurveName.length, (pos += keyName.length))
pubBuf.set(sshCurveName, (pos += 4))
pubBuf.writeUInt32BE(pubBin.length, (pos += sshCurveName.length))
pubBuf.set(pubBin, (pos += 4))
return { sshName: keyName.toString(), priv: privBuf, pub: pubBuf }
}
case 'ed25519': {
// Parse public key
let reader = new Ber.Reader(pub)
reader.readSequence()
// - Algorithm
reader.readSequence()
if (reader.readOID() !== '1.3.101.112') throw new Error('Bad ED25519 public OID')
// - Attributes (absent for ED25519)
let pubBin = reader.readString(Ber.BitString, true)
{
// Remove leading zero bytes
let i = 0
for (; i < pubBin.length && pubBin[i] === 0x00; ++i);
if (i > 0) pubBin = pubBin.slice(i)
}
// Parse private key
reader = new Ber.Reader(priv)
reader.readSequence()
// - Version
if (reader.readInt() !== 0) throw new Error('Unsupported version in ED25519 private key')
// - Algorithm
reader.readSequence()
if (reader.readOID() !== '1.3.101.112') throw new Error('Bad ED25519 private OID')
// - Attributes (absent)
reader = new Ber.Reader(reader.readString(Ber.OctetString, true))
const privBin = reader.readString(Ber.OctetString, true)
/*
OpenSSH ed25519 private key:
string "ssh-ed25519"
string public key
string private key + public key
*/
const keyName = Buffer.from('ssh-ed25519')
const privBuf = Buffer.allocUnsafe(
4 + keyName.length + 4 + pubBin.length + 4 + (privBin.length + pubBin.length)
)
let pos = 0
privBuf.writeUInt32BE(keyName.length, (pos += 0))
privBuf.set(keyName, (pos += 4))
privBuf.writeUInt32BE(pubBin.length, (pos += keyName.length))
privBuf.set(pubBin, (pos += 4))
privBuf.writeUInt32BE(privBin.length + pubBin.length, (pos += pubBin.length))
privBuf.set(privBin, (pos += 4))
privBuf.set(pubBin, (pos += privBin.length))
/*
OpenSSH ed25519 public key:
string "ssh-ed25519"
string public key
*/
const pubBuf = Buffer.allocUnsafe(4 + keyName.length + 4 + pubBin.length)
pos = 0
pubBuf.writeUInt32BE(keyName.length, (pos += 0))
pubBuf.set(keyName, (pos += 4))
pubBuf.writeUInt32BE(pubBin.length, (pos += keyName.length))
pubBuf.set(pubBin, (pos += 4))
return { sshName: keyName.toString(), priv: privBuf, pub: pubBuf }
}
}
}
function convertKeys(keyType, pub, priv, opts) {
let format = 'new'
let encrypted
let comment = ''
if (typeof opts === 'object' && opts !== null) {
if (typeof opts.comment === 'string' && opts.comment) comment = opts.comment
if (typeof opts.format === 'string' && opts.format) format = opts.format
if (opts.passphrase) {
let passphrase
if (typeof opts.passphrase === 'string') passphrase = Buffer.from(opts.passphrase)
else if (Buffer.isBuffer(opts.passphrase)) passphrase = opts.passphrase
else throw new Error('Invalid passphrase')
if (opts.cipher === undefined) throw new Error('Missing cipher name')
const cipher = ciphers.get(opts.cipher)
if (cipher === undefined) throw new Error('Invalid cipher name')
if (format === 'new') {
let rounds = DEFAULT_ROUNDS
if (opts.rounds !== undefined) {
if (!Number.isInteger(opts.rounds)) throw new TypeError('rounds must be an integer')
if (opts.rounds > 0) rounds = opts.rounds
}
const gen = Buffer.allocUnsafe(cipher.keyLen + cipher.ivLen)
const salt = randomBytes(SALT_LEN)
const r = bcrypt_pbkdf(
passphrase,
passphrase.length,
salt,
salt.length,
gen,
gen.length,
rounds
)
if (r !== 0) return new Error('Failed to generate information to encrypt key')
/*
string salt
uint32 rounds
*/
const kdfOptions = Buffer.allocUnsafe(4 + salt.length + 4)
{
let pos = 0
kdfOptions.writeUInt32BE(salt.length, (pos += 0))
kdfOptions.set(salt, (pos += 4))
kdfOptions.writeUInt32BE(rounds, (pos += salt.length))
}
encrypted = {
cipher,
cipherName: opts.cipher,
kdfName: 'bcrypt',
kdfOptions,
key: gen.slice(0, cipher.keyLen),
iv: gen.slice(cipher.keyLen)
}
}
}
}
switch (format) {
case 'new': {
let privateB64 = '-----BEGIN OPENSSH PRIVATE KEY-----\n'
let publicB64
/*
byte[] "openssh-key-v1\0"
string ciphername
string kdfname
string kdfoptions
uint32 number of keys N
string publickey1
string encrypted, padded list of private keys
uint32 checkint
uint32 checkint
byte[] privatekey1
string comment1
byte 1
byte 2
byte 3
...
byte padlen % 255
*/
const cipherName = Buffer.from(encrypted ? encrypted.cipherName : 'none')
const kdfName = Buffer.from(encrypted ? encrypted.kdfName : 'none')
const kdfOptions = encrypted ? encrypted.kdfOptions : Buffer.alloc(0)
const blockLen = encrypted ? encrypted.cipher.blockLen : 8
const parsed = parseDERs(keyType, pub, priv)
const checkInt = randomBytes(4)
const commentBin = Buffer.from(comment)
const privBlobLen = 4 + 4 + parsed.priv.length + 4 + commentBin.length
let padding = []
for (let i = 1; (privBlobLen + padding.length) % blockLen; ++i) padding.push(i & 0xff)
padding = Buffer.from(padding)
let privBlob = Buffer.allocUnsafe(privBlobLen + padding.length)
let extra
{
let pos = 0
privBlob.set(checkInt, (pos += 0))
privBlob.set(checkInt, (pos += 4))
privBlob.set(parsed.priv, (pos += 4))
privBlob.writeUInt32BE(commentBin.length, (pos += parsed.priv.length))
privBlob.set(commentBin, (pos += 4))
privBlob.set(padding, (pos += commentBin.length))
}
if (encrypted) {
const options = { authTagLength: encrypted.cipher.authLen }
const cipher = createCipheriv(
encrypted.cipher.sslName,
encrypted.key,
encrypted.iv,
options
)
cipher.setAutoPadding(false)
privBlob = Buffer.concat([cipher.update(privBlob), cipher.final()])
if (encrypted.cipher.authLen > 0) extra = cipher.getAuthTag()
else extra = Buffer.alloc(0)
encrypted.key.fill(0)
encrypted.iv.fill(0)
} else {
extra = Buffer.alloc(0)
}
const magicBytes = Buffer.from('openssh-key-v1\0')
const privBin = Buffer.allocUnsafe(
magicBytes.length +
4 +
cipherName.length +
4 +
kdfName.length +
4 +
kdfOptions.length +
4 +
4 +
parsed.pub.length +
4 +
privBlob.length +
extra.length
)
{
let pos = 0
privBin.set(magicBytes, (pos += 0))
privBin.writeUInt32BE(cipherName.length, (pos += magicBytes.length))
privBin.set(cipherName, (pos += 4))
privBin.writeUInt32BE(kdfName.length, (pos += cipherName.length))
privBin.set(kdfName, (pos += 4))
privBin.writeUInt32BE(kdfOptions.length, (pos += kdfName.length))
privBin.set(kdfOptions, (pos += 4))
privBin.writeUInt32BE(1, (pos += kdfOptions.length))
privBin.writeUInt32BE(parsed.pub.length, (pos += 4))
privBin.set(parsed.pub, (pos += 4))
privBin.writeUInt32BE(privBlob.length, (pos += parsed.pub.length))
privBin.set(privBlob, (pos += 4))
privBin.set(extra, (pos += privBlob.length))
}
{
const b64 = privBin.base64Slice(0, privBin.length)
let formatted = b64.replace(/.{64}/g, '$&\n')
if (b64.length & 63) formatted += '\n'
privateB64 += formatted
}
{
const b64 = parsed.pub.base64Slice(0, parsed.pub.length)
publicB64 = `${parsed.sshName} ${b64}${comment ? ` ${comment}` : ''}`
}
privateB64 += '-----END OPENSSH PRIVATE KEY-----\n'
return {
private: privateB64,
public: publicB64
}
}
default:
throw new Error('Invalid output key format')
}
}
function noop() {}
module.exports = {
generateKeyPair: (keyType, opts, cb) => {
if (typeof opts === 'function') {
cb = opts
opts = undefined
}
if (typeof cb !== 'function') cb = noop
const args = makeArgs(keyType, opts)
generateKeyPair_(...args, (err, pub, priv) => {
if (err) return cb(err)
let ret
try {
ret = convertKeys(args[0], pub, priv, opts)
} catch (ex) {
return cb(ex)
}
cb(null, ret)
})
},
generateKeyPairSync: (keyType, opts) => {
const args = makeArgs(keyType, opts)
const { publicKey: pub, privateKey: priv } = generateKeyPairSync_(...args)
return convertKeys(args[0], pub, priv, opts)
}
}