154 lines
4.6 KiB
JavaScript
154 lines
4.6 KiB
JavaScript
'use strict'
|
|
|
|
const { Ber } = require('asn1')
|
|
const { Buffer: Asn1Buffer } = require('safer-buffer')
|
|
const { Buffer: RuntimeBuffer } = require('buffer')
|
|
|
|
const X25519_OID = '1.3.101.110'
|
|
|
|
function getNacl() {
|
|
return require('tweetnacl')
|
|
}
|
|
|
|
/** @param {import('safer-buffer').Buffer | Uint8Array} buf */
|
|
function u8(buf) {
|
|
const b = Asn1Buffer.from(buf)
|
|
return new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
|
|
}
|
|
|
|
function berSpkiX25519(pub32) {
|
|
const pub = Asn1Buffer.from(pub32)
|
|
const w = new Ber.Writer()
|
|
w.startSequence()
|
|
w.startSequence()
|
|
w.writeOID(X25519_OID)
|
|
w.endSequence()
|
|
w.startSequence(Ber.BitString)
|
|
w.writeByte(0x00)
|
|
w._ensure(pub.length)
|
|
w._buf.set(pub, w._offset)
|
|
w._offset += pub.length
|
|
w.endSequence()
|
|
w.endSequence()
|
|
return w.buffer
|
|
}
|
|
|
|
function berPkcs8X25519(priv32) {
|
|
const priv = Asn1Buffer.from(priv32)
|
|
const w = new Ber.Writer()
|
|
w.startSequence()
|
|
w.writeInt(0x00, Ber.Integer)
|
|
w.startSequence()
|
|
w.writeOID(X25519_OID)
|
|
w.endSequence()
|
|
w.startSequence(Ber.OctetString)
|
|
w.writeBuffer(priv, Ber.OctetString)
|
|
w.endSequence()
|
|
w.endSequence()
|
|
return w.buffer
|
|
}
|
|
|
|
/** Match bare-ssh2 kex: SPKI DER ends with 32-byte X25519 public key. */
|
|
function parseSpkiX25519PubDer(der) {
|
|
const b = Asn1Buffer.from(der)
|
|
if (b.length < 32) throw new Error('Invalid X25519 SPKI DER')
|
|
return u8(b.slice(-32))
|
|
}
|
|
|
|
function parsePkcs8X25519PrivDer(der) {
|
|
const { Ber: BerR } = require('asn1')
|
|
let reader = new BerR.Reader(Asn1Buffer.from(der))
|
|
reader.readSequence()
|
|
if (reader.readInt() !== 0) throw new Error('Invalid PKCS#8 version')
|
|
reader.readSequence()
|
|
if (reader.readOID() !== X25519_OID) throw new Error('Invalid X25519 PKCS#8 OID')
|
|
reader = new BerR.Reader(reader.readString(BerR.OctetString, true))
|
|
return u8(reader.readString(BerR.OctetString, true))
|
|
}
|
|
|
|
class X25519PublicKey {
|
|
/** @param {Uint8Array | import('safer-buffer').Buffer} pub32 */
|
|
constructor(pub32) {
|
|
this.type = 'public'
|
|
this.asymmetricKeyType = 'x25519'
|
|
this._pub = Asn1Buffer.from(pub32)
|
|
if (this._pub.length !== 32) throw new Error('X25519 public key must be 32 bytes')
|
|
}
|
|
|
|
/** @param {{ type: string, format: string }} opts */
|
|
export(opts) {
|
|
if (!opts || opts.format !== 'der') throw new Error('unsupported export format')
|
|
if (opts.type === 'spki') return berSpkiX25519(this._pub)
|
|
if (opts.type === 'raw') return Asn1Buffer.from(this._pub)
|
|
throw new Error('unsupported export type')
|
|
}
|
|
}
|
|
|
|
class X25519PrivateKey {
|
|
/**
|
|
* @param {Uint8Array | import('safer-buffer').Buffer} priv32
|
|
* @param {Uint8Array | import('safer-buffer').Buffer | null} pub32
|
|
*/
|
|
constructor(priv32, pub32) {
|
|
this.type = 'private'
|
|
this.asymmetricKeyType = 'x25519'
|
|
this._priv = Asn1Buffer.from(priv32)
|
|
if (this._priv.length !== 32) throw new Error('X25519 private key must be 32 bytes')
|
|
this._pub = pub32 ? Asn1Buffer.from(pub32) : null
|
|
}
|
|
|
|
/** @param {{ type: string, format: string }} opts */
|
|
export(opts) {
|
|
if (!opts || opts.format !== 'der') throw new Error('unsupported export format')
|
|
if (opts.type === 'pkcs8') return berPkcs8X25519(this._priv)
|
|
throw new Error('unsupported export type')
|
|
}
|
|
}
|
|
|
|
function generateKeyPairSync() {
|
|
const nacl = getNacl()
|
|
const pair = nacl.box.keyPair()
|
|
return {
|
|
publicKey: new X25519PublicKey(pair.publicKey),
|
|
privateKey: new X25519PrivateKey(pair.secretKey, pair.publicKey)
|
|
}
|
|
}
|
|
|
|
/** @param {import('safer-buffer').Buffer} der */
|
|
function createPublicKeyFromSpkiDer(der) {
|
|
return new X25519PublicKey(parseSpkiX25519PubDer(der))
|
|
}
|
|
|
|
/**
|
|
* @param {{ privateKey: unknown, publicKey: unknown }} opts
|
|
* @returns {import('safer-buffer').Buffer}
|
|
*/
|
|
function diffieHellman(opts) {
|
|
const nacl = getNacl()
|
|
let privU8
|
|
let pubU8
|
|
const sk = opts.privateKey
|
|
const pk = opts.publicKey
|
|
if (sk instanceof X25519PrivateKey) privU8 = u8(sk._priv)
|
|
else if (sk && typeof sk.export === 'function')
|
|
privU8 = parsePkcs8X25519PrivDer(sk.export({ type: 'pkcs8', format: 'der' }))
|
|
else throw new Error('diffieHellman: unsupported private key')
|
|
|
|
if (pk instanceof X25519PublicKey) pubU8 = u8(pk._pub)
|
|
else if (pk && typeof pk.export === 'function')
|
|
pubU8 = parseSpkiX25519PubDer(pk.export({ type: 'spki', format: 'der' }))
|
|
else throw new Error('diffieHellman: unsupported public key')
|
|
|
|
const shared = nacl.scalarMult(privU8, pubU8)
|
|
// Use runtime `buffer` (Bare node-buffer) so kex `convertToMpint` / `hash.update` agree on `Buffer.isBuffer`.
|
|
return RuntimeBuffer.from(shared)
|
|
}
|
|
|
|
module.exports = {
|
|
generateKeyPairSync,
|
|
createPublicKeyFromSpkiDer,
|
|
diffieHellman,
|
|
X25519PublicKey,
|
|
X25519PrivateKey
|
|
}
|