'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 }