UNPKG

@microsoft/dev-tunnels-ssh-keys

Version:

SSH key import/export library for Dev Tunnels

403 lines 19.9 kB
"use strict"; // // Copyright (c) Microsoft Corporation. All rights reserved. // Object.defineProperty(exports, "__esModule", { value: true }); exports.Pkcs8KeyFormatter = void 0; const dev_tunnels_ssh_1 = require("@microsoft/dev-tunnels-ssh"); const keyFormatter_1 = require("./keyFormatter"); const keyData_1 = require("./keyData"); /** Provides import/export of the PKCS#8 key format. */ // eslint-disable-next-line no-redeclare class Pkcs8KeyFormatter { constructor() { /** Mapping from public key algorithm OID to import handler for that algorithm. */ this.importers = new Map(); /** Mapping from public key algorithm name to export handler for that algorithm. */ this.exporters = new Map(); /** Enables overriding randomness for predictable testing. */ this.random = dev_tunnels_ssh_1.SshAlgorithms.random; this.importers.set("1.2.840.113549.1.1.1" /* Oids.rsa */, Pkcs8KeyFormatter.importRsaKey); this.importers.set("1.2.840.10045.2.1" /* Oids.ec */, Pkcs8KeyFormatter.importECKey); this.exporters.set(dev_tunnels_ssh_1.Rsa.keyAlgorithmName, Pkcs8KeyFormatter.exportRsaKey); this.exporters.set(dev_tunnels_ssh_1.ECDsa.ecdsaSha2Nistp256, Pkcs8KeyFormatter.exportECKey); this.exporters.set(dev_tunnels_ssh_1.ECDsa.ecdsaSha2Nistp384, Pkcs8KeyFormatter.exportECKey); this.exporters.set(dev_tunnels_ssh_1.ECDsa.ecdsaSha2Nistp521, Pkcs8KeyFormatter.exportECKey); } async import(keyData) { if (!keyData) throw new TypeError('KeyData object expected.'); if (!keyData.keyType) { // Automatically determine public or private by reading the first few bytes. try { const reader = new dev_tunnels_ssh_1.DerReader(keyData.data); if (reader.peek() === (32 /* DerType.Constructed */ | 16 /* DerType.Sequence */)) { keyData.keyType = Pkcs8KeyFormatter.publicKeyType; } else if (reader.peek() === 2 /* DerType.Integer */) { keyData.keyType = Pkcs8KeyFormatter.privateKeyType; } } catch (e) { return null; } } if (keyData.keyType === Pkcs8KeyFormatter.publicKeyType) { return await this.importPublic(keyData); } else if (keyData.keyType === Pkcs8KeyFormatter.privateKeyType) { return await this.importPrivate(keyData); } else if (keyData.keyType === Pkcs8KeyFormatter.encryptedPrivateKeyType) { throw new Error('Decrypt before importing.'); } return null; } async export(keyPair, includePrivate) { if (!keyPair) throw new TypeError('KeyPair object expected.'); if (includePrivate) { if (!keyPair.hasPrivateKey) { throw new Error('KeyPair object does not contain the private key.'); } return await this.exportPrivate(keyPair); } else { return await this.exportPublic(keyPair); } } async decrypt(keyData, passphrase) { if (!keyData) throw new TypeError('KeyData object expected.'); if (keyData.keyType === Pkcs8KeyFormatter.publicKeyType || keyData.keyType === Pkcs8KeyFormatter.privateKeyType || (!keyData.keyType && !passphrase)) { return keyData; } else if (keyData.keyType === Pkcs8KeyFormatter.encryptedPrivateKeyType || (!keyData.keyType && passphrase)) { if (!passphrase) { throw new Error('A passphrase is required to decrypt the key.'); } return Pkcs8KeyFormatter.decryptPrivate(keyData, passphrase); } return null; } async encrypt(keyData, passphrase) { if (!keyData) throw new TypeError('KeyData object expected.'); if (keyData.keyType === Pkcs8KeyFormatter.publicKeyType) { throw new Error('Public key cannot be encrypted.'); } else if (keyData.keyType === Pkcs8KeyFormatter.privateKeyType) { return Pkcs8KeyFormatter.encryptPrivate(keyData, passphrase, this.random); } else if (keyData.keyType === Pkcs8KeyFormatter.encryptedPrivateKeyType) { throw new Error('Already encrypted.'); } else { throw new Error(`Unsupported key type: ${keyData.keyType}`); } } async importPublic(keyData) { const reader = new dev_tunnels_ssh_1.DerReader(keyData.data); const oidReader = reader.readSequence(); const keyAlgorithm = oidReader.readObjectIdentifier(); const keyBytes = reader.readBitString(); const importer = this.importers.get(keyAlgorithm); if (!importer) { throw new Error(`No PKCS#8 importer available for key algorithm: ${keyAlgorithm}`); } return await importer(keyBytes, oidReader, false); } async importPrivate(keyData) { const reader = new dev_tunnels_ssh_1.DerReader(keyData.data); const version = reader.readInteger().toInt32(); if (version !== 0) { throw new Error(`PKCS#8 format version not supported: ${version}`); } const oidReader = reader.readSequence(); const keyAlgorithm = oidReader.readObjectIdentifier(); const keyBytes = reader.readOctetString(); const importer = this.importers.get(keyAlgorithm); if (!importer) { throw new Error(`No PKCS#8 importer available for key algorithm: ${keyAlgorithm}`); } return await importer(keyBytes, oidReader, true); } static async importRsaKey(keyBytes, oidReader, includePrivate) { const keyReader = new dev_tunnels_ssh_1.DerReader(keyBytes); if (includePrivate) { const version = keyReader.readInteger().toInt32(); if (version !== 0) { throw new Error(`PKCS#8 key format version not supported: ${version}`); } } const parameters = { modulus: keyReader.readInteger(), exponent: keyReader.readInteger(), }; if (includePrivate) { parameters.d = keyReader.readInteger(); parameters.p = keyReader.readInteger(); parameters.q = keyReader.readInteger(); parameters.dp = keyReader.readInteger(); parameters.dq = keyReader.readInteger(); parameters.qi = keyReader.readInteger(); } const keyPair = dev_tunnels_ssh_1.SshAlgorithms.publicKey.rsaWithSha512.createKeyPair(); await keyPair.importParameters(parameters); return keyPair; } static async importECKey(keyBytes, oidReader, includePrivate) { const curveOid = oidReader.readObjectIdentifier(); let publicKeyBytes; let privateKeyBytes = null; if (includePrivate) { const keyReader = new dev_tunnels_ssh_1.DerReader(keyBytes); const version = keyReader.readInteger().toInt32(); if (version !== 1) { throw new Error(`PKCS#8 EC key format version not supported: ${version}`); } privateKeyBytes = keyReader.readOctetString(); const publicKeyReader = keyReader.tryReadTagged(1); if (!publicKeyReader) { throw new Error('Failed to read EC public key data.'); } publicKeyBytes = publicKeyReader.readBitString(); } else { publicKeyBytes = keyBytes; } if (publicKeyBytes.length % 2 !== 1) { throw new Error(`Unexpected key data length: ${publicKeyBytes.length}`); } // 4 = uncompressed curve format const dataFormat = publicKeyBytes[0]; if (dataFormat !== 4) { throw new Error(`Unexpected curve format: ${dataFormat}`); } // X and Y parameters are equal length, after a one-byte header. const x = dev_tunnels_ssh_1.BigInt.fromBytes(publicKeyBytes.slice(1, 1 + (publicKeyBytes.length - 1) / 2), { unsigned: true, }); const y = dev_tunnels_ssh_1.BigInt.fromBytes(publicKeyBytes.slice(1 + (publicKeyBytes.length - 1) / 2), { unsigned: true, }); const d = privateKeyBytes ? dev_tunnels_ssh_1.BigInt.fromBytes(privateKeyBytes, { unsigned: true }) : undefined; const parameters = { curve: { oid: curveOid }, x, y, d, }; const keyPair = new dev_tunnels_ssh_1.ECDsa.KeyPair(); await keyPair.importParameters(parameters); return keyPair; } async exportPublic(keyPair) { const exporter = this.exporters.get(keyPair.keyAlgorithmName); if (!exporter) { throw new Error(`No PKCS#8 exporter available for key algorithm: ${keyPair.keyAlgorithmName}`); } const oidWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(256)); const keyBytes = await exporter(keyPair, oidWriter, false); const writer = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(1024)); writer.writeSequence(oidWriter); writer.writeBitString(keyBytes); const keyData = new keyData_1.KeyData(); keyData.keyType = Pkcs8KeyFormatter.publicKeyType; keyData.data = writer.toBuffer(); return keyData; } async exportPrivate(keyPair) { const exporter = this.exporters.get(keyPair.keyAlgorithmName); if (!exporter) { throw new Error(`No PKCS#8 exporter available for key algorithm: ${keyPair.keyAlgorithmName}`); } const oidWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(256)); const keyBytes = await exporter(keyPair, oidWriter, true); const writer = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(2048)); writer.writeInteger(dev_tunnels_ssh_1.BigInt.fromInt32(0)); // version writer.writeSequence(oidWriter); writer.writeOctetString(keyBytes); return new keyData_1.KeyData(Pkcs8KeyFormatter.privateKeyType, writer.toBuffer()); } static async exportRsaKey(keyPair, oidWriter, includePrivate) { const parameters = await keyPair.exportParameters(); oidWriter.writeObjectIdentifier("1.2.840.113549.1.1.1" /* Oids.rsa */); oidWriter.writeNull(); const keyWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(1024)); if (includePrivate) { keyWriter.writeInteger(dev_tunnels_ssh_1.BigInt.fromInt32(0)); // version } keyWriter.writeInteger(parameters.modulus); keyWriter.writeInteger(parameters.exponent); if (includePrivate) { keyWriter.writeInteger(parameters.d); keyWriter.writeInteger(parameters.p); keyWriter.writeInteger(parameters.q); keyWriter.writeInteger(parameters.dp); keyWriter.writeInteger(parameters.dq); keyWriter.writeInteger(parameters.qi); } return keyWriter.toBuffer(); } static async exportECKey(keyPair, oidWriter, includePrivate) { const parameters = await keyPair.exportParameters(); const curve = dev_tunnels_ssh_1.ECDsa.curves.find((c) => c.oid === parameters.curve.oid); const keySizeInBytes = Math.ceil(curve.keySize / 8); oidWriter.writeObjectIdentifier("1.2.840.10045.2.1" /* Oids.ec */); oidWriter.writeObjectIdentifier(parameters.curve.oid); const x = parameters.x.toBytes({ unsigned: true, length: keySizeInBytes }); const y = parameters.y.toBytes({ unsigned: true, length: keySizeInBytes }); const publicKeyData = Buffer.alloc(1 + x.length + y.length); publicKeyData[0] = 4; // Indicates uncompressed curve format x.copy(publicKeyData, 1); y.copy(publicKeyData, 1 + x.length); if (includePrivate) { const keyWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(512)); keyWriter.writeInteger(dev_tunnels_ssh_1.BigInt.fromInt32(1)); // version keyWriter.writeOctetString(parameters.d.toBytes({ unsigned: true })); const publicKeyWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(1024)); publicKeyWriter.writeBitString(publicKeyData); keyWriter.writeTagged(1, publicKeyWriter); return keyWriter.toBuffer(); } else { return publicKeyData; } } static async decryptPrivate(keyData, passphrase) { let reader = new dev_tunnels_ssh_1.DerReader(keyData.data); const innerReader = reader.readSequence(); let privateKeyData = reader.readOctetString(); reader = innerReader; reader.readObjectIdentifier("1.2.840.113549.1.5.13" /* Oids.pkcs5PBES2 */); reader = reader.readSequence(); let kdfReader = reader.readSequence(); const algReader = reader.readSequence(); kdfReader.readObjectIdentifier("1.2.840.113549.1.5.12" /* Oids.pkcs5PBKDF2 */); kdfReader = kdfReader.readSequence(); const salt = kdfReader.readOctetString(); const iterations = kdfReader.readInteger().toInt32(); kdfReader = kdfReader.readSequence(); kdfReader.readObjectIdentifier("1.2.840.113549.2.9" /* Oids.hmacWithSHA256 */); kdfReader.readNull(); const algorithmOid = algReader.readObjectIdentifier(); const iv = algReader.readOctetString(); const encryption = Pkcs8KeyFormatter.getKeyEncryptionAlgorithm(algorithmOid); const key = await Pkcs8KeyFormatter.pbkdf2(Buffer.from(passphrase, 'utf8'), salt, iterations, encryption.keyLength); const decipher = await encryption.createCipher(false, key, iv); try { privateKeyData = await decipher.transform(privateKeyData); } catch (e) { // Web crypto AES-CBC may throw an error due to invalid padding, if the key is incorrect. privateKeyData = Buffer.alloc(0); } finally { decipher.dispose(); } // The first part of the key should be a DER sequence header. if (privateKeyData[0] !== (32 /* DerType.Constructed */ | 16 /* DerType.Sequence */)) { throw new Error('Key decryption failed - incorrect passphrase.'); } return new keyData_1.KeyData(Pkcs8KeyFormatter.privateKeyType, privateKeyData); } static async encryptPrivate(keyData, passphrase, random) { let privateKeyData = Buffer.from(keyData.data); const encryption = Pkcs8KeyFormatter.getKeyEncryptionAlgorithm("2.16.840.1.101.3.4.1.42" /* Oids.aes256Cbc */); const salt = Buffer.alloc(8); random.getBytes(salt); const iterations = 2048; const key = await Pkcs8KeyFormatter.pbkdf2(Buffer.from(passphrase, 'utf8'), salt, iterations, encryption.keyLength); const iv = Buffer.alloc(encryption.blockLength); random.getBytes(iv); // Append PKCS#7 padding up to next block boundary. const paddingLength = encryption.blockLength - (privateKeyData.length % encryption.blockLength); const paddedData = Buffer.alloc(privateKeyData.length + paddingLength); privateKeyData.copy(paddedData, 0); for (let i = privateKeyData.length; i < paddedData.length; i++) { paddedData[i] = paddingLength; } privateKeyData = paddedData; const cipher = await encryption.createCipher(true, key, iv); try { privateKeyData = await cipher.transform(privateKeyData); } finally { cipher.dispose(); } const pbeWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(256)); pbeWriter.writeObjectIdentifier("1.2.840.113549.1.5.13" /* Oids.pkcs5PBES2 */); const kdfAndAlgWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(256)); const kdfWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(256)); kdfWriter.writeObjectIdentifier("1.2.840.113549.1.5.12" /* Oids.pkcs5PBKDF2 */); const kdfParamsWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(32)); kdfParamsWriter.writeOctetString(salt); kdfParamsWriter.writeInteger(dev_tunnels_ssh_1.BigInt.fromInt32(iterations)); const hmacWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(16)); hmacWriter.writeObjectIdentifier("1.2.840.113549.2.9" /* Oids.hmacWithSHA256 */); hmacWriter.writeNull(); kdfParamsWriter.writeSequence(hmacWriter); kdfWriter.writeSequence(kdfParamsWriter); kdfAndAlgWriter.writeSequence(kdfWriter); const algWriter = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(64)); algWriter.writeObjectIdentifier("2.16.840.1.101.3.4.1.42" /* Oids.aes256Cbc */); algWriter.writeOctetString(iv); kdfAndAlgWriter.writeSequence(algWriter); pbeWriter.writeSequence(kdfAndAlgWriter); const writer = new dev_tunnels_ssh_1.DerWriter(Buffer.alloc(2048)); writer.writeSequence(pbeWriter); writer.writeOctetString(privateKeyData); return new keyData_1.KeyData(Pkcs8KeyFormatter.encryptedPrivateKeyType, writer.toBuffer()); } static getKeyEncryptionAlgorithm(algorithmOid) { // Note algorithms other than AES256 are used only for decrypting (importing) keys. if (algorithmOid === "2.16.840.1.101.3.4.1.42" /* Oids.aes256Cbc */) { return new dev_tunnels_ssh_1.Encryption('aes256-cbc', 'AES', 'CBC', 256); } else if (algorithmOid === "2.16.840.1.101.3.4.1.22" /* Oids.aes192Cbc */) { return new dev_tunnels_ssh_1.Encryption('aes192-cbc', 'AES', 'CBC', 192); } else if (algorithmOid === "2.16.840.1.101.3.4.1.2" /* Oids.aes128Cbc */) { return new dev_tunnels_ssh_1.Encryption('aes128-cbc', 'AES', 'CBC', 128); } else if (algorithmOid === "1.2.840.113549.3.7" /* Oids.desEde3Cbc */) { return new dev_tunnels_ssh_1.Encryption('3des-cbc', '3DES', 'CBC', 192); } else { throw new Error(`Key cipher not supported: ${algorithmOid}`); } } static async pbkdf2(passphrase, salt, iterations, keyLength) { if ((0, keyFormatter_1.useWebCrypto)()) { const passphraseKey = await crypto.subtle.importKey('raw', passphrase, 'PBKDF2', false, // extractable ['deriveBits']); const key = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations, hash: 'SHA-256', }, passphraseKey, keyLength * 8); return Buffer.from(key); } else { const crypto = await Promise.resolve().then(() => require('crypto')); return await new Promise((resolve, reject) => { crypto.pbkdf2(passphrase, salt, iterations, keyLength, 'sha256', (err, derivedKey) => { if (err) reject(err); else resolve(derivedKey); }); }); } } } exports.Pkcs8KeyFormatter = Pkcs8KeyFormatter; Pkcs8KeyFormatter.publicKeyType = 'PUBLIC KEY'; Pkcs8KeyFormatter.privateKeyType = 'PRIVATE KEY'; Pkcs8KeyFormatter.encryptedPrivateKeyType = 'ENCRYPTED PRIVATE KEY'; //# sourceMappingURL=pkcs8KeyFormatter.js.map