@microsoft/dev-tunnels-ssh-keys
Version:
SSH key import/export library for Dev Tunnels
403 lines • 19.9 kB
JavaScript
//
// 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
;