1371 lines
44 KiB
JavaScript
1371 lines
44 KiB
JavaScript
// TODO:
|
|
// * utilize `crypto.create(Private|Public)Key()` and `keyObject.export()`
|
|
// * handle multi-line header values (OpenSSH)?
|
|
// * more thorough validation?
|
|
'use strict'
|
|
|
|
require('../buffer-polyfill.js')
|
|
|
|
const {
|
|
createDecipheriv,
|
|
createECDH,
|
|
createHash,
|
|
createHmac,
|
|
createSign,
|
|
createVerify,
|
|
getCiphers,
|
|
sign: sign_,
|
|
verify: verify_
|
|
} = require('../../shims/crypto/index.js')
|
|
const supportedOpenSSLCiphers = getCiphers()
|
|
|
|
const { Ber } = require('asn1')
|
|
const bcrypt_pbkdf = require('bcrypt-pbkdf').pbkdf
|
|
|
|
const { CIPHER_INFO } = require('./crypto.js')
|
|
const { eddsaSupported, SUPPORTED_CIPHER } = require('./constants.js')
|
|
const {
|
|
bufferSlice,
|
|
makeBufferParser,
|
|
readString,
|
|
readUInt32BE,
|
|
writeUInt32BE
|
|
} = require('./utils.js')
|
|
|
|
const SYM_HASH_ALGO = Symbol('Hash Algorithm')
|
|
const SYM_PRIV_PEM = Symbol('Private key PEM')
|
|
const SYM_PUB_PEM = Symbol('Public key PEM')
|
|
const SYM_PUB_SSH = Symbol('Public key SSH')
|
|
const SYM_DECRYPTED = Symbol('Decrypted Key')
|
|
|
|
// Create OpenSSL cipher name -> SSH cipher name conversion table
|
|
const CIPHER_INFO_OPENSSL = Object.create(null)
|
|
{
|
|
const keys = Object.keys(CIPHER_INFO)
|
|
for (let i = 0; i < keys.length; ++i) {
|
|
const cipherName = CIPHER_INFO[keys[i]].sslName
|
|
if (!cipherName || CIPHER_INFO_OPENSSL[cipherName]) continue
|
|
CIPHER_INFO_OPENSSL[cipherName] = CIPHER_INFO[keys[i]]
|
|
}
|
|
}
|
|
|
|
const binaryKeyParser = makeBufferParser()
|
|
|
|
function makePEM(type, data) {
|
|
data = Buffer.from(data).toString('base64')
|
|
let formatted = data.replace(/.{64}/g, '$&\n')
|
|
if (data.length & 63) formatted += '\n'
|
|
return `-----BEGIN ${type} KEY-----\n${formatted}-----END ${type} KEY-----`
|
|
}
|
|
|
|
function combineBuffers(buf1, buf2) {
|
|
const result = Buffer.allocUnsafe(buf1.length + buf2.length)
|
|
result.set(buf1, 0)
|
|
result.set(buf2, buf1.length)
|
|
return result
|
|
}
|
|
|
|
function skipFields(buf, nfields) {
|
|
const bufLen = buf.length
|
|
let pos = buf._pos || 0
|
|
for (let i = 0; i < nfields; ++i) {
|
|
const left = bufLen - pos
|
|
if (pos >= bufLen || left < 4) return false
|
|
const len = readUInt32BE(buf, pos)
|
|
if (left < 4 + len) return false
|
|
pos += 4 + len
|
|
}
|
|
buf._pos = pos
|
|
return true
|
|
}
|
|
|
|
function genOpenSSLRSAPub(n, e) {
|
|
const asnWriter = new Ber.Writer()
|
|
asnWriter.startSequence()
|
|
// algorithm
|
|
asnWriter.startSequence()
|
|
asnWriter.writeOID('1.2.840.113549.1.1.1') // rsaEncryption
|
|
// algorithm parameters (RSA has none)
|
|
asnWriter.writeNull()
|
|
asnWriter.endSequence()
|
|
|
|
// subjectPublicKey
|
|
asnWriter.startSequence(Ber.BitString)
|
|
asnWriter.writeByte(0x00)
|
|
asnWriter.startSequence()
|
|
asnWriter.writeBuffer(n, Ber.Integer)
|
|
asnWriter.writeBuffer(e, Ber.Integer)
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
return makePEM('PUBLIC', asnWriter.buffer)
|
|
}
|
|
|
|
function genOpenSSHRSAPub(n, e) {
|
|
const publicKey = Buffer.allocUnsafe(4 + 7 + 4 + e.length + 4 + n.length)
|
|
|
|
writeUInt32BE(publicKey, 7, 0)
|
|
publicKey.utf8Write('ssh-rsa', 4, 7)
|
|
|
|
let i = 4 + 7
|
|
writeUInt32BE(publicKey, e.length, i)
|
|
publicKey.set(e, (i += 4))
|
|
|
|
writeUInt32BE(publicKey, n.length, (i += e.length))
|
|
publicKey.set(n, i + 4)
|
|
|
|
return publicKey
|
|
}
|
|
|
|
const genOpenSSLRSAPriv = (() => {
|
|
function genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp) {
|
|
const asnWriter = new Ber.Writer()
|
|
asnWriter.startSequence()
|
|
asnWriter.writeInt(0x00, Ber.Integer)
|
|
asnWriter.writeBuffer(n, Ber.Integer)
|
|
asnWriter.writeBuffer(e, Ber.Integer)
|
|
asnWriter.writeBuffer(d, Ber.Integer)
|
|
asnWriter.writeBuffer(p, Ber.Integer)
|
|
asnWriter.writeBuffer(q, Ber.Integer)
|
|
asnWriter.writeBuffer(dmp1, Ber.Integer)
|
|
asnWriter.writeBuffer(dmq1, Ber.Integer)
|
|
asnWriter.writeBuffer(iqmp, Ber.Integer)
|
|
asnWriter.endSequence()
|
|
return asnWriter.buffer
|
|
}
|
|
|
|
function bigIntFromBuffer(buf) {
|
|
return BigInt(`0x${buf.hexSlice(0, buf.length)}`)
|
|
}
|
|
|
|
function bigIntToBuffer(bn) {
|
|
let hex = bn.toString(16)
|
|
if ((hex.length & 1) !== 0) {
|
|
hex = `0${hex}`
|
|
} else {
|
|
const sigbit = hex.charCodeAt(0)
|
|
// BER/DER integers require leading zero byte to denote a positive value
|
|
// when first byte >= 0x80
|
|
if (
|
|
sigbit === 56 /* '8' */ ||
|
|
sigbit === 57 /* '9' */ ||
|
|
(sigbit >= 97 /* 'a' */ && sigbit <= 102) /* 'f' */
|
|
) {
|
|
hex = `00${hex}`
|
|
}
|
|
}
|
|
return Buffer.from(hex, 'hex')
|
|
}
|
|
|
|
return function genOpenSSLRSAPriv(n, e, d, iqmp, p, q) {
|
|
const bn_d = bigIntFromBuffer(d)
|
|
const dmp1 = bigIntToBuffer(bn_d % (bigIntFromBuffer(p) - 1n))
|
|
const dmq1 = bigIntToBuffer(bn_d % (bigIntFromBuffer(q) - 1n))
|
|
return makePEM('RSA PRIVATE', genRSAASN1Buf(n, e, d, p, q, dmp1, dmq1, iqmp))
|
|
}
|
|
})()
|
|
|
|
function genOpenSSLDSAPub(p, q, g, y) {
|
|
const asnWriter = new Ber.Writer()
|
|
asnWriter.startSequence()
|
|
// algorithm
|
|
asnWriter.startSequence()
|
|
asnWriter.writeOID('1.2.840.10040.4.1') // id-dsa
|
|
// algorithm parameters
|
|
asnWriter.startSequence()
|
|
asnWriter.writeBuffer(p, Ber.Integer)
|
|
asnWriter.writeBuffer(q, Ber.Integer)
|
|
asnWriter.writeBuffer(g, Ber.Integer)
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
|
|
// subjectPublicKey
|
|
asnWriter.startSequence(Ber.BitString)
|
|
asnWriter.writeByte(0x00)
|
|
asnWriter.writeBuffer(y, Ber.Integer)
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
return makePEM('PUBLIC', asnWriter.buffer)
|
|
}
|
|
|
|
function genOpenSSHDSAPub(p, q, g, y) {
|
|
const publicKey = Buffer.allocUnsafe(
|
|
4 + 7 + 4 + p.length + 4 + q.length + 4 + g.length + 4 + y.length
|
|
)
|
|
|
|
writeUInt32BE(publicKey, 7, 0)
|
|
publicKey.utf8Write('ssh-dss', 4, 7)
|
|
|
|
let i = 4 + 7
|
|
writeUInt32BE(publicKey, p.length, i)
|
|
publicKey.set(p, (i += 4))
|
|
|
|
writeUInt32BE(publicKey, q.length, (i += p.length))
|
|
publicKey.set(q, (i += 4))
|
|
|
|
writeUInt32BE(publicKey, g.length, (i += q.length))
|
|
publicKey.set(g, (i += 4))
|
|
|
|
writeUInt32BE(publicKey, y.length, (i += g.length))
|
|
publicKey.set(y, i + 4)
|
|
|
|
return publicKey
|
|
}
|
|
|
|
function genOpenSSLDSAPriv(p, q, g, y, x) {
|
|
const asnWriter = new Ber.Writer()
|
|
asnWriter.startSequence()
|
|
asnWriter.writeInt(0x00, Ber.Integer)
|
|
asnWriter.writeBuffer(p, Ber.Integer)
|
|
asnWriter.writeBuffer(q, Ber.Integer)
|
|
asnWriter.writeBuffer(g, Ber.Integer)
|
|
asnWriter.writeBuffer(y, Ber.Integer)
|
|
asnWriter.writeBuffer(x, Ber.Integer)
|
|
asnWriter.endSequence()
|
|
return makePEM('DSA PRIVATE', asnWriter.buffer)
|
|
}
|
|
|
|
function genOpenSSLEdPub(pub) {
|
|
const asnWriter = new Ber.Writer()
|
|
asnWriter.startSequence()
|
|
// algorithm
|
|
asnWriter.startSequence()
|
|
asnWriter.writeOID('1.3.101.112') // id-Ed25519
|
|
asnWriter.endSequence()
|
|
|
|
// PublicKey
|
|
asnWriter.startSequence(Ber.BitString)
|
|
asnWriter.writeByte(0x00)
|
|
// XXX: hack to write a raw buffer without a tag -- yuck
|
|
asnWriter._ensure(pub.length)
|
|
asnWriter._buf.set(pub, asnWriter._offset)
|
|
asnWriter._offset += pub.length
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
return makePEM('PUBLIC', asnWriter.buffer)
|
|
}
|
|
|
|
function genOpenSSHEdPub(pub) {
|
|
const publicKey = Buffer.allocUnsafe(4 + 11 + 4 + pub.length)
|
|
|
|
writeUInt32BE(publicKey, 11, 0)
|
|
publicKey.utf8Write('ssh-ed25519', 4, 11)
|
|
|
|
writeUInt32BE(publicKey, pub.length, 15)
|
|
publicKey.set(pub, 19)
|
|
|
|
return publicKey
|
|
}
|
|
|
|
function genOpenSSLEdPriv(priv) {
|
|
const asnWriter = new Ber.Writer()
|
|
asnWriter.startSequence()
|
|
// version
|
|
asnWriter.writeInt(0x00, Ber.Integer)
|
|
|
|
// algorithm
|
|
asnWriter.startSequence()
|
|
asnWriter.writeOID('1.3.101.112') // id-Ed25519
|
|
asnWriter.endSequence()
|
|
|
|
// PrivateKey
|
|
asnWriter.startSequence(Ber.OctetString)
|
|
asnWriter.writeBuffer(priv, Ber.OctetString)
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
return makePEM('PRIVATE', asnWriter.buffer)
|
|
}
|
|
|
|
function genOpenSSLECDSAPub(oid, Q) {
|
|
const asnWriter = new Ber.Writer()
|
|
asnWriter.startSequence()
|
|
// algorithm
|
|
asnWriter.startSequence()
|
|
asnWriter.writeOID('1.2.840.10045.2.1') // id-ecPublicKey
|
|
// algorithm parameters (namedCurve)
|
|
asnWriter.writeOID(oid)
|
|
asnWriter.endSequence()
|
|
|
|
// subjectPublicKey
|
|
asnWriter.startSequence(Ber.BitString)
|
|
asnWriter.writeByte(0x00)
|
|
// XXX: hack to write a raw buffer without a tag -- yuck
|
|
asnWriter._ensure(Q.length)
|
|
asnWriter._buf.set(Q, asnWriter._offset)
|
|
asnWriter._offset += Q.length
|
|
// end hack
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
return makePEM('PUBLIC', asnWriter.buffer)
|
|
}
|
|
|
|
function genOpenSSHECDSAPub(oid, Q) {
|
|
let curveName
|
|
switch (oid) {
|
|
case '1.2.840.10045.3.1.7':
|
|
// prime256v1/secp256r1
|
|
curveName = 'nistp256'
|
|
break
|
|
case '1.3.132.0.34':
|
|
// secp384r1
|
|
curveName = 'nistp384'
|
|
break
|
|
case '1.3.132.0.35':
|
|
// secp521r1
|
|
curveName = 'nistp521'
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
|
|
const publicKey = Buffer.allocUnsafe(4 + 19 + 4 + 8 + 4 + Q.length)
|
|
|
|
writeUInt32BE(publicKey, 19, 0)
|
|
publicKey.utf8Write(`ecdsa-sha2-${curveName}`, 4, 19)
|
|
|
|
writeUInt32BE(publicKey, 8, 23)
|
|
publicKey.utf8Write(curveName, 27, 8)
|
|
|
|
writeUInt32BE(publicKey, Q.length, 35)
|
|
publicKey.set(Q, 39)
|
|
|
|
return publicKey
|
|
}
|
|
|
|
function genOpenSSLECDSAPriv(oid, pub, priv) {
|
|
const asnWriter = new Ber.Writer()
|
|
asnWriter.startSequence()
|
|
// version
|
|
asnWriter.writeInt(0x01, Ber.Integer)
|
|
// privateKey
|
|
asnWriter.writeBuffer(priv, Ber.OctetString)
|
|
// parameters (optional)
|
|
asnWriter.startSequence(0xa0)
|
|
asnWriter.writeOID(oid)
|
|
asnWriter.endSequence()
|
|
// publicKey (optional)
|
|
asnWriter.startSequence(0xa1)
|
|
asnWriter.startSequence(Ber.BitString)
|
|
asnWriter.writeByte(0x00)
|
|
// XXX: hack to write a raw buffer without a tag -- yuck
|
|
asnWriter._ensure(pub.length)
|
|
asnWriter._buf.set(pub, asnWriter._offset)
|
|
asnWriter._offset += pub.length
|
|
// end hack
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
asnWriter.endSequence()
|
|
return makePEM('EC PRIVATE', asnWriter.buffer)
|
|
}
|
|
|
|
function genOpenSSLECDSAPubFromPriv(curveName, priv) {
|
|
const tempECDH = createECDH(curveName)
|
|
tempECDH.setPrivateKey(priv)
|
|
return tempECDH.getPublicKey()
|
|
}
|
|
|
|
const BaseKey = {
|
|
sign: (() => {
|
|
if (typeof sign_ === 'function') {
|
|
return function sign(data, algo) {
|
|
const pem = this[SYM_PRIV_PEM]
|
|
if (pem === null) return new Error('No private key available')
|
|
if (!algo || typeof algo !== 'string') algo = this[SYM_HASH_ALGO]
|
|
try {
|
|
return sign_(algo, data, pem)
|
|
} catch (ex) {
|
|
return ex
|
|
}
|
|
}
|
|
}
|
|
return function sign(data, algo) {
|
|
const pem = this[SYM_PRIV_PEM]
|
|
if (pem === null) return new Error('No private key available')
|
|
if (!algo || typeof algo !== 'string') algo = this[SYM_HASH_ALGO]
|
|
const signature = createSign(algo)
|
|
signature.update(data)
|
|
try {
|
|
return signature.sign(pem)
|
|
} catch (ex) {
|
|
return ex
|
|
}
|
|
}
|
|
})(),
|
|
verify: (() => {
|
|
if (typeof verify_ === 'function') {
|
|
return function verify(data, signature, algo) {
|
|
const pem = this[SYM_PUB_PEM]
|
|
if (pem === null) return new Error('No public key available')
|
|
if (!algo || typeof algo !== 'string') algo = this[SYM_HASH_ALGO]
|
|
try {
|
|
return verify_(algo, data, pem, signature)
|
|
} catch (ex) {
|
|
return ex
|
|
}
|
|
}
|
|
}
|
|
return function verify(data, signature, algo) {
|
|
const pem = this[SYM_PUB_PEM]
|
|
if (pem === null) return new Error('No public key available')
|
|
if (!algo || typeof algo !== 'string') algo = this[SYM_HASH_ALGO]
|
|
const verifier = createVerify(algo)
|
|
verifier.update(data)
|
|
try {
|
|
return verifier.verify(pem, signature)
|
|
} catch (ex) {
|
|
return ex
|
|
}
|
|
}
|
|
})(),
|
|
isPrivateKey: function isPrivateKey() {
|
|
return this[SYM_PRIV_PEM] !== null
|
|
},
|
|
getPrivatePEM: function getPrivatePEM() {
|
|
return this[SYM_PRIV_PEM]
|
|
},
|
|
getPublicPEM: function getPublicPEM() {
|
|
return this[SYM_PUB_PEM]
|
|
},
|
|
getPublicSSH: function getPublicSSH() {
|
|
return this[SYM_PUB_SSH]
|
|
},
|
|
equals: function equals(key) {
|
|
const parsed = parseKey(key)
|
|
if (parsed instanceof Error) return false
|
|
return (
|
|
this.type === parsed.type &&
|
|
this[SYM_PRIV_PEM] === parsed[SYM_PRIV_PEM] &&
|
|
this[SYM_PUB_PEM] === parsed[SYM_PUB_PEM] &&
|
|
this[SYM_PUB_SSH].equals(parsed[SYM_PUB_SSH])
|
|
)
|
|
}
|
|
}
|
|
|
|
function OpenSSH_Private(type, comment, privPEM, pubPEM, pubSSH, algo, decrypted) {
|
|
this.type = type
|
|
this.comment = comment
|
|
this[SYM_PRIV_PEM] = privPEM
|
|
this[SYM_PUB_PEM] = pubPEM
|
|
this[SYM_PUB_SSH] = pubSSH
|
|
this[SYM_HASH_ALGO] = algo
|
|
this[SYM_DECRYPTED] = decrypted
|
|
}
|
|
OpenSSH_Private.prototype = BaseKey
|
|
{
|
|
const regexp =
|
|
/^-----BEGIN OPENSSH PRIVATE KEY-----(?:\r\n|\n)([\s\S]+)(?:\r\n|\n)-----END OPENSSH PRIVATE KEY-----$/
|
|
OpenSSH_Private.parse = (str, passphrase) => {
|
|
const m = regexp.exec(str)
|
|
if (m === null) return null
|
|
let ret
|
|
const data = Buffer.from(m[1].replace(/\s+/g, ''), 'base64')
|
|
if (data.length < 31)
|
|
// magic (+ magic null term.) + minimum field lengths
|
|
return new Error('Malformed OpenSSH private key')
|
|
const magic = data.toString('utf8', 0, 15)
|
|
if (magic !== 'openssh-key-v1\0') return new Error(`Unsupported OpenSSH key magic: ${magic}`)
|
|
|
|
const cipherName = readString(data, 15, true)
|
|
if (cipherName === undefined) return new Error('Malformed OpenSSH private key')
|
|
if (cipherName !== 'none' && SUPPORTED_CIPHER.indexOf(cipherName) === -1)
|
|
return new Error(`Unsupported cipher for OpenSSH key: ${cipherName}`)
|
|
|
|
const kdfName = readString(data, data._pos, true)
|
|
if (kdfName === undefined) return new Error('Malformed OpenSSH private key')
|
|
if (kdfName !== 'none') {
|
|
if (cipherName === 'none') return new Error('Malformed OpenSSH private key')
|
|
if (kdfName !== 'bcrypt') return new Error(`Unsupported kdf name for OpenSSH key: ${kdfName}`)
|
|
if (!passphrase) {
|
|
return new Error('Encrypted private OpenSSH key detected, but no passphrase given')
|
|
}
|
|
} else if (cipherName !== 'none') {
|
|
return new Error('Malformed OpenSSH private key')
|
|
}
|
|
|
|
let encInfo
|
|
let cipherKey
|
|
let cipherIV
|
|
if (cipherName !== 'none') encInfo = CIPHER_INFO[cipherName]
|
|
const kdfOptions = readString(data, data._pos)
|
|
if (kdfOptions === undefined) return new Error('Malformed OpenSSH private key')
|
|
if (kdfOptions.length) {
|
|
switch (kdfName) {
|
|
case 'none':
|
|
return new Error('Malformed OpenSSH private key')
|
|
case 'bcrypt': {
|
|
/*
|
|
string salt
|
|
uint32 rounds
|
|
*/
|
|
const salt = readString(kdfOptions, 0)
|
|
if (salt === undefined || kdfOptions._pos + 4 > kdfOptions.length)
|
|
return new Error('Malformed OpenSSH private key')
|
|
const rounds = readUInt32BE(kdfOptions, kdfOptions._pos)
|
|
const gen = Buffer.allocUnsafe(encInfo.keyLen + encInfo.ivLen)
|
|
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 decrypt key')
|
|
cipherKey = bufferSlice(gen, 0, encInfo.keyLen)
|
|
cipherIV = bufferSlice(gen, encInfo.keyLen, gen.length)
|
|
break
|
|
}
|
|
}
|
|
} else if (kdfName !== 'none') {
|
|
return new Error('Malformed OpenSSH private key')
|
|
}
|
|
|
|
if (data._pos + 3 >= data.length) return new Error('Malformed OpenSSH private key')
|
|
const keyCount = readUInt32BE(data, data._pos)
|
|
data._pos += 4
|
|
|
|
if (keyCount > 0) {
|
|
// TODO: place sensible limit on max `keyCount`
|
|
|
|
// Read public keys first
|
|
for (let i = 0; i < keyCount; ++i) {
|
|
const pubData = readString(data, data._pos)
|
|
if (pubData === undefined) return new Error('Malformed OpenSSH private key')
|
|
const type = readString(pubData, 0, true)
|
|
if (type === undefined) return new Error('Malformed OpenSSH private key')
|
|
}
|
|
|
|
let privBlob = readString(data, data._pos)
|
|
if (privBlob === undefined) return new Error('Malformed OpenSSH private key')
|
|
|
|
if (cipherKey !== undefined) {
|
|
// Encrypted private key(s)
|
|
if (privBlob.length < encInfo.blockLen || privBlob.length % encInfo.blockLen !== 0) {
|
|
return new Error('Malformed OpenSSH private key')
|
|
}
|
|
try {
|
|
const options = { authTagLength: encInfo.authLen }
|
|
const decipher = createDecipheriv(encInfo.sslName, cipherKey, cipherIV, options)
|
|
try {
|
|
decipher.setAutoPadding(false)
|
|
} catch {
|
|
/* bare-crypto AEAD */
|
|
}
|
|
if (encInfo.authLen > 0) {
|
|
if (data.length - data._pos < encInfo.authLen)
|
|
return new Error('Malformed OpenSSH private key')
|
|
decipher.setAuthTag(bufferSlice(data, data._pos, (data._pos += encInfo.authLen)))
|
|
}
|
|
privBlob = combineBuffers(decipher.update(privBlob), decipher.final())
|
|
} catch (ex) {
|
|
return ex
|
|
}
|
|
}
|
|
// Nothing should we follow the private key(s), except a possible
|
|
// authentication tag for relevant ciphers
|
|
if (data._pos !== data.length) return new Error('Malformed OpenSSH private key')
|
|
|
|
ret = parseOpenSSHPrivKeys(privBlob, keyCount, cipherKey !== undefined)
|
|
} else {
|
|
ret = []
|
|
}
|
|
if (ret instanceof Error) return ret
|
|
// This will need to change if/when OpenSSH ever starts storing multiple
|
|
// keys in their key files
|
|
return ret[0]
|
|
}
|
|
|
|
function parseOpenSSHPrivKeys(data, nkeys, decrypted) {
|
|
const keys = []
|
|
/*
|
|
uint32 checkint
|
|
uint32 checkint
|
|
string privatekey1
|
|
string comment1
|
|
string privatekey2
|
|
string comment2
|
|
...
|
|
string privatekeyN
|
|
string commentN
|
|
char 1
|
|
char 2
|
|
char 3
|
|
...
|
|
char padlen % 255
|
|
*/
|
|
if (data.length < 8) return new Error('Malformed OpenSSH private key')
|
|
const check1 = readUInt32BE(data, 0)
|
|
const check2 = readUInt32BE(data, 4)
|
|
if (check1 !== check2) {
|
|
if (decrypted) {
|
|
return new Error('OpenSSH key integrity check failed -- bad passphrase?')
|
|
}
|
|
return new Error('OpenSSH key integrity check failed')
|
|
}
|
|
data._pos = 8
|
|
let i
|
|
let oid
|
|
for (i = 0; i < nkeys; ++i) {
|
|
let algo
|
|
let privPEM
|
|
let pubPEM
|
|
let pubSSH
|
|
// The OpenSSH documentation for the key format actually lies, the
|
|
// entirety of the private key content is not contained with a string
|
|
// field, it's actually the literal contents of the private key, so to be
|
|
// able to find the end of the key data you need to know the layout/format
|
|
// of each key type ...
|
|
const type = readString(data, data._pos, true)
|
|
if (type === undefined) return new Error('Malformed OpenSSH private key')
|
|
|
|
switch (type) {
|
|
case 'ssh-rsa': {
|
|
/*
|
|
string n -- public
|
|
string e -- public
|
|
string d -- private
|
|
string iqmp -- private
|
|
string p -- private
|
|
string q -- private
|
|
*/
|
|
const n = readString(data, data._pos)
|
|
if (n === undefined) return new Error('Malformed OpenSSH private key')
|
|
const e = readString(data, data._pos)
|
|
if (e === undefined) return new Error('Malformed OpenSSH private key')
|
|
const d = readString(data, data._pos)
|
|
if (d === undefined) return new Error('Malformed OpenSSH private key')
|
|
const iqmp = readString(data, data._pos)
|
|
if (iqmp === undefined) return new Error('Malformed OpenSSH private key')
|
|
const p = readString(data, data._pos)
|
|
if (p === undefined) return new Error('Malformed OpenSSH private key')
|
|
const q = readString(data, data._pos)
|
|
if (q === undefined) return new Error('Malformed OpenSSH private key')
|
|
|
|
pubPEM = genOpenSSLRSAPub(n, e)
|
|
pubSSH = genOpenSSHRSAPub(n, e)
|
|
privPEM = genOpenSSLRSAPriv(n, e, d, iqmp, p, q)
|
|
algo = 'sha1'
|
|
break
|
|
}
|
|
case 'ssh-dss': {
|
|
/*
|
|
string p -- public
|
|
string q -- public
|
|
string g -- public
|
|
string y -- public
|
|
string x -- private
|
|
*/
|
|
const p = readString(data, data._pos)
|
|
if (p === undefined) return new Error('Malformed OpenSSH private key')
|
|
const q = readString(data, data._pos)
|
|
if (q === undefined) return new Error('Malformed OpenSSH private key')
|
|
const g = readString(data, data._pos)
|
|
if (g === undefined) return new Error('Malformed OpenSSH private key')
|
|
const y = readString(data, data._pos)
|
|
if (y === undefined) return new Error('Malformed OpenSSH private key')
|
|
const x = readString(data, data._pos)
|
|
if (x === undefined) return new Error('Malformed OpenSSH private key')
|
|
|
|
pubPEM = genOpenSSLDSAPub(p, q, g, y)
|
|
pubSSH = genOpenSSHDSAPub(p, q, g, y)
|
|
privPEM = genOpenSSLDSAPriv(p, q, g, y, x)
|
|
algo = 'sha1'
|
|
break
|
|
}
|
|
case 'ssh-ed25519': {
|
|
if (!eddsaSupported) return new Error(`Unsupported OpenSSH private key type: ${type}`)
|
|
/*
|
|
* string public key
|
|
* string private key + public key
|
|
*/
|
|
const edpub = readString(data, data._pos)
|
|
if (edpub === undefined || edpub.length !== 32)
|
|
return new Error('Malformed OpenSSH private key')
|
|
const edpriv = readString(data, data._pos)
|
|
if (edpriv === undefined || edpriv.length !== 64)
|
|
return new Error('Malformed OpenSSH private key')
|
|
|
|
pubPEM = genOpenSSLEdPub(edpub)
|
|
pubSSH = genOpenSSHEdPub(edpub)
|
|
privPEM = genOpenSSLEdPriv(bufferSlice(edpriv, 0, 32))
|
|
algo = null
|
|
break
|
|
}
|
|
case 'ecdsa-sha2-nistp256':
|
|
algo = 'sha256'
|
|
oid = '1.2.840.10045.3.1.7'
|
|
// FALLTHROUGH
|
|
case 'ecdsa-sha2-nistp384':
|
|
if (algo === undefined) {
|
|
algo = 'sha384'
|
|
oid = '1.3.132.0.34'
|
|
}
|
|
// FALLTHROUGH
|
|
case 'ecdsa-sha2-nistp521': {
|
|
if (algo === undefined) {
|
|
algo = 'sha512'
|
|
oid = '1.3.132.0.35'
|
|
}
|
|
/*
|
|
string curve name
|
|
string Q -- public
|
|
string d -- private
|
|
*/
|
|
// TODO: validate curve name against type
|
|
if (!skipFields(data, 1))
|
|
// Skip curve name
|
|
return new Error('Malformed OpenSSH private key')
|
|
const ecpub = readString(data, data._pos)
|
|
if (ecpub === undefined) return new Error('Malformed OpenSSH private key')
|
|
const ecpriv = readString(data, data._pos)
|
|
if (ecpriv === undefined) return new Error('Malformed OpenSSH private key')
|
|
|
|
pubPEM = genOpenSSLECDSAPub(oid, ecpub)
|
|
pubSSH = genOpenSSHECDSAPub(oid, ecpub)
|
|
privPEM = genOpenSSLECDSAPriv(oid, ecpub, ecpriv)
|
|
break
|
|
}
|
|
default:
|
|
return new Error(`Unsupported OpenSSH private key type: ${type}`)
|
|
}
|
|
|
|
const privComment = readString(data, data._pos, true)
|
|
if (privComment === undefined) return new Error('Malformed OpenSSH private key')
|
|
|
|
keys.push(new OpenSSH_Private(type, privComment, privPEM, pubPEM, pubSSH, algo, decrypted))
|
|
}
|
|
let cnt = 0
|
|
for (i = data._pos; i < data.length; ++i) {
|
|
if (data[i] !== ++cnt % 255) return new Error('Malformed OpenSSH private key')
|
|
}
|
|
|
|
return keys
|
|
}
|
|
}
|
|
|
|
function OpenSSH_Old_Private(type, comment, privPEM, pubPEM, pubSSH, algo, decrypted) {
|
|
this.type = type
|
|
this.comment = comment
|
|
this[SYM_PRIV_PEM] = privPEM
|
|
this[SYM_PUB_PEM] = pubPEM
|
|
this[SYM_PUB_SSH] = pubSSH
|
|
this[SYM_HASH_ALGO] = algo
|
|
this[SYM_DECRYPTED] = decrypted
|
|
}
|
|
OpenSSH_Old_Private.prototype = BaseKey
|
|
{
|
|
const regexp =
|
|
/^-----BEGIN (RSA|DSA|EC) PRIVATE KEY-----(?:\r\n|\n)((?:[^:]+:\s*[\S].*(?:\r\n|\n))*)([\s\S]+)(?:\r\n|\n)-----END (RSA|DSA|EC) PRIVATE KEY-----$/
|
|
OpenSSH_Old_Private.parse = (str, passphrase) => {
|
|
const m = regexp.exec(str)
|
|
if (m === null) return null
|
|
let privBlob = Buffer.from(m[3].replace(/\s+/g, ''), 'base64')
|
|
let headers = m[2]
|
|
let decrypted = false
|
|
if (headers !== undefined) {
|
|
// encrypted key
|
|
headers = headers.split(/\r\n|\n/g)
|
|
for (let i = 0; i < headers.length; ++i) {
|
|
const header = headers[i]
|
|
let sepIdx = header.indexOf(':')
|
|
if (header.slice(0, sepIdx) === 'DEK-Info') {
|
|
const val = header.slice(sepIdx + 2)
|
|
sepIdx = val.indexOf(',')
|
|
if (sepIdx === -1) continue
|
|
const cipherName = val.slice(0, sepIdx).toLowerCase()
|
|
if (supportedOpenSSLCiphers.indexOf(cipherName) === -1) {
|
|
return new Error(
|
|
`Cipher (${cipherName}) not supported ` + 'for encrypted OpenSSH private key'
|
|
)
|
|
}
|
|
const encInfo = CIPHER_INFO_OPENSSL[cipherName]
|
|
if (!encInfo) {
|
|
return new Error(
|
|
`Cipher (${cipherName}) not supported ` + 'for encrypted OpenSSH private key'
|
|
)
|
|
}
|
|
const cipherIV = Buffer.from(val.slice(sepIdx + 1), 'hex')
|
|
if (cipherIV.length !== encInfo.ivLen)
|
|
return new Error('Malformed encrypted OpenSSH private key')
|
|
if (!passphrase) {
|
|
return new Error('Encrypted OpenSSH private key detected, but no passphrase given')
|
|
}
|
|
const ivSlice = bufferSlice(cipherIV, 0, 8)
|
|
let cipherKey = createHash('md5').update(passphrase).update(ivSlice).digest()
|
|
while (cipherKey.length < encInfo.keyLen) {
|
|
cipherKey = combineBuffers(
|
|
cipherKey,
|
|
createHash('md5').update(cipherKey).update(passphrase).update(ivSlice).digest()
|
|
)
|
|
}
|
|
if (cipherKey.length > encInfo.keyLen)
|
|
cipherKey = bufferSlice(cipherKey, 0, encInfo.keyLen)
|
|
try {
|
|
const decipher = createDecipheriv(cipherName, cipherKey, cipherIV)
|
|
try {
|
|
decipher.setAutoPadding(false)
|
|
} catch {
|
|
/* bare-crypto AEAD */
|
|
}
|
|
privBlob = combineBuffers(decipher.update(privBlob), decipher.final())
|
|
decrypted = true
|
|
} catch (ex) {
|
|
return ex
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let type
|
|
let privPEM
|
|
let pubPEM
|
|
let pubSSH
|
|
let algo
|
|
let reader
|
|
let errMsg = 'Malformed OpenSSH private key'
|
|
if (decrypted) errMsg += '. Bad passphrase?'
|
|
switch (m[1]) {
|
|
case 'RSA':
|
|
type = 'ssh-rsa'
|
|
privPEM = makePEM('RSA PRIVATE', privBlob)
|
|
try {
|
|
reader = new Ber.Reader(privBlob)
|
|
reader.readSequence()
|
|
reader.readInt() // skip version
|
|
const n = reader.readString(Ber.Integer, true)
|
|
if (n === null) return new Error(errMsg)
|
|
const e = reader.readString(Ber.Integer, true)
|
|
if (e === null) return new Error(errMsg)
|
|
pubPEM = genOpenSSLRSAPub(n, e)
|
|
pubSSH = genOpenSSHRSAPub(n, e)
|
|
} catch {
|
|
return new Error(errMsg)
|
|
}
|
|
algo = 'sha1'
|
|
break
|
|
case 'DSA':
|
|
type = 'ssh-dss'
|
|
privPEM = makePEM('DSA PRIVATE', privBlob)
|
|
try {
|
|
reader = new Ber.Reader(privBlob)
|
|
reader.readSequence()
|
|
reader.readInt() // skip version
|
|
const p = reader.readString(Ber.Integer, true)
|
|
if (p === null) return new Error(errMsg)
|
|
const q = reader.readString(Ber.Integer, true)
|
|
if (q === null) return new Error(errMsg)
|
|
const g = reader.readString(Ber.Integer, true)
|
|
if (g === null) return new Error(errMsg)
|
|
const y = reader.readString(Ber.Integer, true)
|
|
if (y === null) return new Error(errMsg)
|
|
pubPEM = genOpenSSLDSAPub(p, q, g, y)
|
|
pubSSH = genOpenSSHDSAPub(p, q, g, y)
|
|
} catch {
|
|
return new Error(errMsg)
|
|
}
|
|
algo = 'sha1'
|
|
break
|
|
case 'EC': {
|
|
let ecSSLName
|
|
let ecPriv
|
|
let ecOID
|
|
try {
|
|
reader = new Ber.Reader(privBlob)
|
|
reader.readSequence()
|
|
reader.readInt() // skip version
|
|
ecPriv = reader.readString(Ber.OctetString, true)
|
|
reader.readByte() // Skip "complex" context type byte
|
|
const offset = reader.readLength() // Skip context length
|
|
if (offset !== null) {
|
|
reader._offset = offset
|
|
ecOID = reader.readOID()
|
|
if (ecOID === null) return new Error(errMsg)
|
|
switch (ecOID) {
|
|
case '1.2.840.10045.3.1.7':
|
|
// prime256v1/secp256r1
|
|
ecSSLName = 'prime256v1'
|
|
type = 'ecdsa-sha2-nistp256'
|
|
algo = 'sha256'
|
|
break
|
|
case '1.3.132.0.34':
|
|
// secp384r1
|
|
ecSSLName = 'secp384r1'
|
|
type = 'ecdsa-sha2-nistp384'
|
|
algo = 'sha384'
|
|
break
|
|
case '1.3.132.0.35':
|
|
// secp521r1
|
|
ecSSLName = 'secp521r1'
|
|
type = 'ecdsa-sha2-nistp521'
|
|
algo = 'sha512'
|
|
break
|
|
default:
|
|
return new Error(`Unsupported private key EC OID: ${ecOID}`)
|
|
}
|
|
} else {
|
|
return new Error(errMsg)
|
|
}
|
|
} catch {
|
|
return new Error(errMsg)
|
|
}
|
|
privPEM = makePEM('EC PRIVATE', privBlob)
|
|
const pubBlob = genOpenSSLECDSAPubFromPriv(ecSSLName, ecPriv)
|
|
pubPEM = genOpenSSLECDSAPub(ecOID, pubBlob)
|
|
pubSSH = genOpenSSHECDSAPub(ecOID, pubBlob)
|
|
break
|
|
}
|
|
}
|
|
|
|
return new OpenSSH_Old_Private(type, '', privPEM, pubPEM, pubSSH, algo, decrypted)
|
|
}
|
|
}
|
|
|
|
function PPK_Private(type, comment, privPEM, pubPEM, pubSSH, algo, decrypted) {
|
|
this.type = type
|
|
this.comment = comment
|
|
this[SYM_PRIV_PEM] = privPEM
|
|
this[SYM_PUB_PEM] = pubPEM
|
|
this[SYM_PUB_SSH] = pubSSH
|
|
this[SYM_HASH_ALGO] = algo
|
|
this[SYM_DECRYPTED] = decrypted
|
|
}
|
|
PPK_Private.prototype = BaseKey
|
|
{
|
|
const EMPTY_PASSPHRASE = Buffer.alloc(0)
|
|
const PPK_IV = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
|
const PPK_PP1 = Buffer.from([0, 0, 0, 0])
|
|
const PPK_PP2 = Buffer.from([0, 0, 0, 1])
|
|
const regexp =
|
|
/^PuTTY-User-Key-File-2: (ssh-(?:rsa|dss))\r?\nEncryption: (aes256-cbc|none)\r?\nComment: ([^\r\n]*)\r?\nPublic-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-MAC: ([^\r\n]+)/
|
|
PPK_Private.parse = (str, passphrase) => {
|
|
const m = regexp.exec(str)
|
|
if (m === null) return null
|
|
// m[1] = key type
|
|
// m[2] = encryption type
|
|
// m[3] = comment
|
|
// m[4] = base64-encoded public key data:
|
|
// for "ssh-rsa":
|
|
// string "ssh-rsa"
|
|
// mpint e (public exponent)
|
|
// mpint n (modulus)
|
|
// for "ssh-dss":
|
|
// string "ssh-dss"
|
|
// mpint p (modulus)
|
|
// mpint q (prime)
|
|
// mpint g (base number)
|
|
// mpint y (public key parameter: g^x mod p)
|
|
// m[5] = base64-encoded private key data:
|
|
// for "ssh-rsa":
|
|
// mpint d (private exponent)
|
|
// mpint p (prime 1)
|
|
// mpint q (prime 2)
|
|
// mpint iqmp ([inverse of q] mod p)
|
|
// for "ssh-dss":
|
|
// mpint x (private key parameter)
|
|
// m[6] = SHA1 HMAC over:
|
|
// string name of algorithm ("ssh-dss", "ssh-rsa")
|
|
// string encryption type
|
|
// string comment
|
|
// string public key data
|
|
// string private-plaintext (including the final padding)
|
|
const cipherName = m[2]
|
|
const encrypted = cipherName !== 'none'
|
|
if (encrypted && !passphrase) {
|
|
return new Error('Encrypted PPK private key detected, but no passphrase given')
|
|
}
|
|
|
|
let privBlob = Buffer.from(m[5].replace(/\s+/g, ''), 'base64')
|
|
|
|
if (encrypted) {
|
|
const encInfo = CIPHER_INFO[cipherName]
|
|
let cipherKey = combineBuffers(
|
|
createHash('sha1').update(PPK_PP1).update(passphrase).digest(),
|
|
createHash('sha1').update(PPK_PP2).update(passphrase).digest()
|
|
)
|
|
if (cipherKey.length > encInfo.keyLen) cipherKey = bufferSlice(cipherKey, 0, encInfo.keyLen)
|
|
try {
|
|
const decipher = createDecipheriv(encInfo.sslName, cipherKey, PPK_IV)
|
|
try {
|
|
decipher.setAutoPadding(false)
|
|
} catch {
|
|
/* bare-crypto AEAD */
|
|
}
|
|
privBlob = combineBuffers(decipher.update(privBlob), decipher.final())
|
|
} catch (ex) {
|
|
return ex
|
|
}
|
|
}
|
|
|
|
const type = m[1]
|
|
const comment = m[3]
|
|
const pubBlob = Buffer.from(m[4].replace(/\s+/g, ''), 'base64')
|
|
|
|
const mac = m[6]
|
|
const typeLen = type.length
|
|
const cipherNameLen = cipherName.length
|
|
const commentLen = Buffer.byteLength(comment)
|
|
const pubLen = pubBlob.length
|
|
const privLen = privBlob.length
|
|
const macData = Buffer.allocUnsafe(
|
|
4 + typeLen + 4 + cipherNameLen + 4 + commentLen + 4 + pubLen + 4 + privLen
|
|
)
|
|
let p = 0
|
|
|
|
writeUInt32BE(macData, typeLen, p)
|
|
macData.utf8Write(type, (p += 4), typeLen)
|
|
writeUInt32BE(macData, cipherNameLen, (p += typeLen))
|
|
macData.utf8Write(cipherName, (p += 4), cipherNameLen)
|
|
writeUInt32BE(macData, commentLen, (p += cipherNameLen))
|
|
macData.utf8Write(comment, (p += 4), commentLen)
|
|
writeUInt32BE(macData, pubLen, (p += commentLen))
|
|
macData.set(pubBlob, (p += 4))
|
|
writeUInt32BE(macData, privLen, (p += pubLen))
|
|
macData.set(privBlob, p + 4)
|
|
|
|
if (!passphrase) passphrase = EMPTY_PASSPHRASE
|
|
|
|
const calcMAC = createHmac(
|
|
'sha1',
|
|
createHash('sha1').update('putty-private-key-file-mac-key').update(passphrase).digest()
|
|
)
|
|
.update(macData)
|
|
.digest('hex')
|
|
|
|
if (calcMAC !== mac) {
|
|
if (encrypted) {
|
|
return new Error('PPK private key integrity check failed -- bad passphrase?')
|
|
}
|
|
return new Error('PPK private key integrity check failed')
|
|
}
|
|
|
|
let pubPEM
|
|
let pubSSH
|
|
let privPEM
|
|
pubBlob._pos = 0
|
|
skipFields(pubBlob, 1) // skip (duplicate) key type
|
|
switch (type) {
|
|
case 'ssh-rsa': {
|
|
const e = readString(pubBlob, pubBlob._pos)
|
|
if (e === undefined) return new Error('Malformed PPK public key')
|
|
const n = readString(pubBlob, pubBlob._pos)
|
|
if (n === undefined) return new Error('Malformed PPK public key')
|
|
const d = readString(privBlob, 0)
|
|
if (d === undefined) return new Error('Malformed PPK private key')
|
|
const p = readString(privBlob, privBlob._pos)
|
|
if (p === undefined) return new Error('Malformed PPK private key')
|
|
const q = readString(privBlob, privBlob._pos)
|
|
if (q === undefined) return new Error('Malformed PPK private key')
|
|
const iqmp = readString(privBlob, privBlob._pos)
|
|
if (iqmp === undefined) return new Error('Malformed PPK private key')
|
|
pubPEM = genOpenSSLRSAPub(n, e)
|
|
pubSSH = genOpenSSHRSAPub(n, e)
|
|
privPEM = genOpenSSLRSAPriv(n, e, d, iqmp, p, q)
|
|
break
|
|
}
|
|
case 'ssh-dss': {
|
|
const p = readString(pubBlob, pubBlob._pos)
|
|
if (p === undefined) return new Error('Malformed PPK public key')
|
|
const q = readString(pubBlob, pubBlob._pos)
|
|
if (q === undefined) return new Error('Malformed PPK public key')
|
|
const g = readString(pubBlob, pubBlob._pos)
|
|
if (g === undefined) return new Error('Malformed PPK public key')
|
|
const y = readString(pubBlob, pubBlob._pos)
|
|
if (y === undefined) return new Error('Malformed PPK public key')
|
|
const x = readString(privBlob, 0)
|
|
if (x === undefined) return new Error('Malformed PPK private key')
|
|
|
|
pubPEM = genOpenSSLDSAPub(p, q, g, y)
|
|
pubSSH = genOpenSSHDSAPub(p, q, g, y)
|
|
privPEM = genOpenSSLDSAPriv(p, q, g, y, x)
|
|
break
|
|
}
|
|
}
|
|
|
|
return new PPK_Private(type, comment, privPEM, pubPEM, pubSSH, 'sha1', encrypted)
|
|
}
|
|
}
|
|
|
|
function OpenSSH_Public(type, comment, pubPEM, pubSSH, algo) {
|
|
this.type = type
|
|
this.comment = comment
|
|
this[SYM_PRIV_PEM] = null
|
|
this[SYM_PUB_PEM] = pubPEM
|
|
this[SYM_PUB_SSH] = pubSSH
|
|
this[SYM_HASH_ALGO] = algo
|
|
this[SYM_DECRYPTED] = false
|
|
}
|
|
OpenSSH_Public.prototype = BaseKey
|
|
{
|
|
let regexp
|
|
if (eddsaSupported)
|
|
regexp =
|
|
/^(((?:ssh-(?:rsa|dss|ed25519))|ecdsa-sha2-nistp(?:256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z/+=]+)(?:$|\s+([\S].*)?)$/
|
|
else
|
|
regexp =
|
|
/^(((?:ssh-(?:rsa|dss))|ecdsa-sha2-nistp(?:256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z/+=]+)(?:$|\s+([\S].*)?)$/
|
|
OpenSSH_Public.parse = (str) => {
|
|
const m = regexp.exec(str)
|
|
if (m === null) return null
|
|
// m[1] = full type
|
|
// m[2] = base type
|
|
// m[3] = base64-encoded public key
|
|
// m[4] = comment
|
|
|
|
const fullType = m[1]
|
|
const baseType = m[2]
|
|
const data = Buffer.from(m[3].replace(/\s+/g, ''), 'base64')
|
|
const comment = m[4] || ''
|
|
|
|
const type = readString(data, data._pos, true)
|
|
if (type === undefined || type.indexOf(baseType) !== 0)
|
|
return new Error('Malformed OpenSSH public key')
|
|
|
|
return parseDER(data, baseType, comment, fullType)
|
|
}
|
|
}
|
|
|
|
function RFC4716_Public(type, comment, pubPEM, pubSSH, algo) {
|
|
this.type = type
|
|
this.comment = comment
|
|
this[SYM_PRIV_PEM] = null
|
|
this[SYM_PUB_PEM] = pubPEM
|
|
this[SYM_PUB_SSH] = pubSSH
|
|
this[SYM_HASH_ALGO] = algo
|
|
this[SYM_DECRYPTED] = false
|
|
}
|
|
RFC4716_Public.prototype = BaseKey
|
|
{
|
|
const regexp =
|
|
/^---- BEGIN SSH2 PUBLIC KEY ----(?:\r?\n)((?:.{0,72}\r?\n)+)---- END SSH2 PUBLIC KEY ----$/
|
|
const RE_DATA = /^[A-Z0-9a-z/+=\r\n]+$/
|
|
const RE_HEADER = /^([\x21-\x39\x3B-\x7E]{1,64}): ((?:[^\\]*\\\r?\n)*[^\r\n]+)\r?\n/gm
|
|
const RE_HEADER_ENDS = /\\\r?\n/g
|
|
RFC4716_Public.parse = (str) => {
|
|
let m = regexp.exec(str)
|
|
if (m === null) return null
|
|
|
|
const body = m[1]
|
|
let dataStart = 0
|
|
let comment = ''
|
|
|
|
while ((m = RE_HEADER.exec(body))) {
|
|
const headerName = m[1]
|
|
const headerValue = m[2].replace(RE_HEADER_ENDS, '')
|
|
if (headerValue.length > 1024) {
|
|
RE_HEADER.lastIndex = 0
|
|
return new Error('Malformed RFC4716 public key')
|
|
}
|
|
|
|
dataStart = RE_HEADER.lastIndex
|
|
|
|
if (headerName.toLowerCase() === 'comment') {
|
|
comment = headerValue
|
|
if (
|
|
comment.length > 1 &&
|
|
comment.charCodeAt(0) === 34 /* '"' */ &&
|
|
comment.charCodeAt(comment.length - 1) === 34 /* '"' */
|
|
) {
|
|
comment = comment.slice(1, -1)
|
|
}
|
|
}
|
|
}
|
|
|
|
let data = body.slice(dataStart)
|
|
if (!RE_DATA.test(data)) return new Error('Malformed RFC4716 public key')
|
|
|
|
data = Buffer.from(data, 'base64')
|
|
|
|
const type = readString(data, 0, true)
|
|
if (type === undefined) return new Error('Malformed RFC4716 public key')
|
|
|
|
let pubPEM = null
|
|
let pubSSH = null
|
|
switch (type) {
|
|
case 'ssh-rsa': {
|
|
const e = readString(data, data._pos)
|
|
if (e === undefined) return new Error('Malformed RFC4716 public key')
|
|
const n = readString(data, data._pos)
|
|
if (n === undefined) return new Error('Malformed RFC4716 public key')
|
|
pubPEM = genOpenSSLRSAPub(n, e)
|
|
pubSSH = genOpenSSHRSAPub(n, e)
|
|
break
|
|
}
|
|
case 'ssh-dss': {
|
|
const p = readString(data, data._pos)
|
|
if (p === undefined) return new Error('Malformed RFC4716 public key')
|
|
const q = readString(data, data._pos)
|
|
if (q === undefined) return new Error('Malformed RFC4716 public key')
|
|
const g = readString(data, data._pos)
|
|
if (g === undefined) return new Error('Malformed RFC4716 public key')
|
|
const y = readString(data, data._pos)
|
|
if (y === undefined) return new Error('Malformed RFC4716 public key')
|
|
pubPEM = genOpenSSLDSAPub(p, q, g, y)
|
|
pubSSH = genOpenSSHDSAPub(p, q, g, y)
|
|
break
|
|
}
|
|
default:
|
|
return new Error('Malformed RFC4716 public key')
|
|
}
|
|
|
|
return new RFC4716_Public(type, comment, pubPEM, pubSSH, 'sha1')
|
|
}
|
|
}
|
|
|
|
function parseDER(data, baseType, comment, fullType) {
|
|
if (!isSupportedKeyType(baseType))
|
|
return new Error(`Unsupported OpenSSH public key type: ${baseType}`)
|
|
|
|
let algo
|
|
let oid
|
|
let pubPEM = null
|
|
let pubSSH = null
|
|
|
|
switch (baseType) {
|
|
case 'ssh-rsa': {
|
|
const e = readString(data, data._pos || 0)
|
|
if (e === undefined) return new Error('Malformed OpenSSH public key')
|
|
const n = readString(data, data._pos)
|
|
if (n === undefined) return new Error('Malformed OpenSSH public key')
|
|
pubPEM = genOpenSSLRSAPub(n, e)
|
|
pubSSH = genOpenSSHRSAPub(n, e)
|
|
algo = 'sha1'
|
|
break
|
|
}
|
|
case 'ssh-dss': {
|
|
const p = readString(data, data._pos || 0)
|
|
if (p === undefined) return new Error('Malformed OpenSSH public key')
|
|
const q = readString(data, data._pos)
|
|
if (q === undefined) return new Error('Malformed OpenSSH public key')
|
|
const g = readString(data, data._pos)
|
|
if (g === undefined) return new Error('Malformed OpenSSH public key')
|
|
const y = readString(data, data._pos)
|
|
if (y === undefined) return new Error('Malformed OpenSSH public key')
|
|
pubPEM = genOpenSSLDSAPub(p, q, g, y)
|
|
pubSSH = genOpenSSHDSAPub(p, q, g, y)
|
|
algo = 'sha1'
|
|
break
|
|
}
|
|
case 'ssh-ed25519': {
|
|
const edpub = readString(data, data._pos || 0)
|
|
if (edpub === undefined || edpub.length !== 32)
|
|
return new Error('Malformed OpenSSH public key')
|
|
pubPEM = genOpenSSLEdPub(edpub)
|
|
pubSSH = genOpenSSHEdPub(edpub)
|
|
algo = null
|
|
break
|
|
}
|
|
case 'ecdsa-sha2-nistp256':
|
|
algo = 'sha256'
|
|
oid = '1.2.840.10045.3.1.7'
|
|
// FALLTHROUGH
|
|
case 'ecdsa-sha2-nistp384':
|
|
if (algo === undefined) {
|
|
algo = 'sha384'
|
|
oid = '1.3.132.0.34'
|
|
}
|
|
// FALLTHROUGH
|
|
case 'ecdsa-sha2-nistp521': {
|
|
if (algo === undefined) {
|
|
algo = 'sha512'
|
|
oid = '1.3.132.0.35'
|
|
}
|
|
// TODO: validate curve name against type
|
|
if (!skipFields(data, 1))
|
|
// Skip curve name
|
|
return new Error('Malformed OpenSSH public key')
|
|
const ecpub = readString(data, data._pos || 0)
|
|
if (ecpub === undefined) return new Error('Malformed OpenSSH public key')
|
|
pubPEM = genOpenSSLECDSAPub(oid, ecpub)
|
|
pubSSH = genOpenSSHECDSAPub(oid, ecpub)
|
|
break
|
|
}
|
|
default:
|
|
return new Error(`Unsupported OpenSSH public key type: ${baseType}`)
|
|
}
|
|
|
|
return new OpenSSH_Public(fullType, comment, pubPEM, pubSSH, algo)
|
|
}
|
|
|
|
function isSupportedKeyType(type) {
|
|
switch (type) {
|
|
case 'ssh-rsa':
|
|
case 'ssh-dss':
|
|
case 'ecdsa-sha2-nistp256':
|
|
case 'ecdsa-sha2-nistp384':
|
|
case 'ecdsa-sha2-nistp521':
|
|
return true
|
|
case 'ssh-ed25519':
|
|
if (eddsaSupported) return true
|
|
// FALLTHROUGH
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
function isParsedKey(val) {
|
|
if (!val) return false
|
|
return typeof val[SYM_DECRYPTED] === 'boolean'
|
|
}
|
|
|
|
function parseKey(data, passphrase) {
|
|
if (isParsedKey(data)) return data
|
|
|
|
let origBuffer
|
|
if (Buffer.isBuffer(data)) {
|
|
origBuffer = data
|
|
data = data.toString('utf8').trim()
|
|
} else if (typeof data === 'string') {
|
|
data = data.trim()
|
|
} else {
|
|
return new Error('Key data must be a Buffer or string')
|
|
}
|
|
|
|
// eslint-disable-next-line eqeqeq
|
|
if (passphrase != undefined) {
|
|
if (typeof passphrase === 'string') passphrase = Buffer.from(passphrase)
|
|
else if (!Buffer.isBuffer(passphrase))
|
|
return new Error('Passphrase must be a string or Buffer when supplied')
|
|
}
|
|
|
|
let ret
|
|
|
|
// First try as printable string format (e.g. PEM)
|
|
|
|
// Private keys
|
|
if ((ret = OpenSSH_Private.parse(data, passphrase)) !== null) return ret
|
|
if ((ret = OpenSSH_Old_Private.parse(data, passphrase)) !== null) return ret
|
|
if ((ret = PPK_Private.parse(data, passphrase)) !== null) return ret
|
|
|
|
// Public keys
|
|
if ((ret = OpenSSH_Public.parse(data)) !== null) return ret
|
|
if ((ret = RFC4716_Public.parse(data)) !== null) return ret
|
|
|
|
// Finally try as a binary format if we were originally passed binary data
|
|
if (origBuffer) {
|
|
binaryKeyParser.init(origBuffer, 0)
|
|
const type = binaryKeyParser.readString(true)
|
|
if (type !== undefined) {
|
|
data = binaryKeyParser.readRaw()
|
|
if (data !== undefined) {
|
|
ret = parseDER(data, type, '', type)
|
|
// Ignore potentially useless errors in case the data was not actually
|
|
// in the binary format
|
|
if (ret instanceof Error) ret = null
|
|
}
|
|
}
|
|
binaryKeyParser.clear()
|
|
}
|
|
|
|
if (ret) return ret
|
|
|
|
return new Error('Unsupported key format')
|
|
}
|
|
|
|
module.exports = {
|
|
isParsedKey,
|
|
isSupportedKeyType,
|
|
parseDERKey: (data, type) => parseDER(data, type, '', type),
|
|
parseKey
|
|
}
|