/**
* Copyright (C) 2017-2018 Regents of the University of California.
* @author: Jeff Thompson <[email protected]>
* @author: From https://github.com/named-data/ndn-cxx/blob/master/src/security/transform/private-key.cpp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* A copy of the GNU Lesser General Public License is in the file COPYING.
*/
// Use capitalized Crypto to not clash with the browser's crypto.subtle.
/** @ignore */
var constants = require('constants'); /** @ignore */
var Crypto = require('../../crypto.js'); /** @ignore */
var KeyType = require('../security-types').KeyType; /** @ignore */
var EncryptAlgorithmType = require('../../encrypt/algo/encrypt-params.js').EncryptAlgorithmType; /** @ignore */
var DigestAlgorithm = require('../security-types.js').DigestAlgorithm; /** @ignore */
var DataUtils = require('../../encoding/data-utils.js').DataUtils; /** @ignore */
var SyncPromise = require('../../util/sync-promise.js').SyncPromise; /** @ignore */
var DerNode = require('../../encoding/der/der-node.js').DerNode; /** @ignore */
var DerInteger = require('../../encoding/der/der-node.js').DerNode.DerInteger; /** @ignore */
var OID = require('../../encoding/oid.js').OID; /** @ignore */
var Blob = require('../../util/blob.js').Blob; /** @ignore */
var UseSubtleCrypto = require('../../use-subtle-crypto-node.js').UseSubtleCrypto; /** @ignore */
var rsaKeygen = null;
try {
// This should be installed with: sudo npm install rsa-keygen
rsaKeygen = require('rsa-keygen');
}
catch (e) {}
/**
* A TpmPrivateKey holds an in-memory private key and provides cryptographic
* operations such as for signing by the in-memory TPM.
*
* Create an uninitialized TpmPrivateKey. You must call a load method to
* initialize it, such as loadPkcs1.
* @constructor
*/
var TpmPrivateKey = function TpmPrivateKey()
{
this.keyType_ = null; // number from KeyType
this.privateKey_ = null; // The PEM-encoded private key.
};
exports.TpmPrivateKey = TpmPrivateKey;
/**
* Create a new TpmPrivateKey.Error to report an error in private key processing.
* Call with: throw new TpmPrivateKey.Error(new Error("message")).
* @constructor
* @param {Error} error The exception created with new Error.
*/
TpmPrivateKey.Error = function TpmPrivateKeyError(error)
{
if (error) {
error.__proto__ = TpmPrivateKey.Error.prototype;
return error;
}
};
TpmPrivateKey.Error.prototype = new Error();
TpmPrivateKey.Error.prototype.name = "TpmPrivateKeyError";
/**
* Load the unencrypted private key from a buffer with the PKCS #1 encoding.
* This replaces any existing private key in this object.
* @param {Buffer} encoding The byte buffer with the private key encoding.
* @param {number} keyType (optional) The KeyType, such as KeyType.RSA. If
* omitted or null, then partially decode the private key to determine the key
* type.
* @throws TpmPrivateKey.Error for errors decoding the key.
*/
TpmPrivateKey.prototype.loadPkcs1 = function(encoding, keyType)
{
if (encoding instanceof Blob)
encoding = encoding.buf();
if (keyType == undefined) {
// Try to determine the key type.
try {
var parsedNode = DerNode.parse(encoding);
var children = parsedNode.getChildren();
// An RsaPrivateKey has integer version 0 and 8 integers.
if (children.length == 9 &&
(children[0] instanceof DerInteger) &&
children[0].toVal() == 0 &&
(children[1] instanceof DerInteger) &&
(children[2] instanceof DerInteger) &&
(children[3] instanceof DerInteger) &&
(children[4] instanceof DerInteger) &&
(children[5] instanceof DerInteger) &&
(children[6] instanceof DerInteger) &&
(children[7] instanceof DerInteger) &&
(children[8] instanceof DerInteger))
keyType = KeyType.RSA;
else
// Assume it is an EC key. Try decoding it below.
keyType = KeyType.EC;
} catch (ex) {
// Assume it is an EC key. Try decoding it below.
keyType = KeyType.EC;
}
}
if (keyType == KeyType.EC) {
// Encode the DER as PEM.
var keyBase64 = encoding.toString('base64');
var keyPem = "-----BEGIN EC PRIVATE KEY-----\n";
for (var i = 0; i < keyBase64.length; i += 64)
keyPem += (keyBase64.substr(i, 64) + "\n");
keyPem += "-----END EC PRIVATE KEY-----";
this.privateKey_ = keyPem;
}
else if (keyType == KeyType.RSA) {
// Encode the DER as PEM.
var keyBase64 = encoding.toString('base64');
var keyPem = "-----BEGIN RSA PRIVATE KEY-----\n";
for (var i = 0; i < keyBase64.length; i += 64)
keyPem += (keyBase64.substr(i, 64) + "\n");
keyPem += "-----END RSA PRIVATE KEY-----";
this.privateKey_ = keyPem;
}
else
throw new TpmPrivateKey.Error(new Error
("loadPkcs1: Unrecognized keyType: " + keyType));
this.keyType_ = keyType;
};
/**
* Load the unencrypted private key from a buffer with the PKCS #8 encoding.
* This replaces any existing private key in this object.
* @param {Buffer} encoding The byte buffer with the private key encoding.
* @param {number} keyType (optional) The KeyType, such as KeyType.RSA. If
* omitted or null, then partially decode the private key to determine the key
* type.
* @throws TpmPrivateKey.Error for errors decoding the key.
*/
TpmPrivateKey.prototype.loadPkcs8 = function(encoding, keyType)
{
if (encoding instanceof Blob)
encoding = encoding.buf();
if (keyType == undefined) {
// Decode the PKCS #8 private key to find the algorithm OID and the inner
// private key DER.
var oidString, algorithmParameters, privateKeyDer;
try {
var parsedNode = DerNode.parse(encoding);
var pkcs8Children = parsedNode.getChildren();
// Get the algorithm OID and parameters.
var algorithmIdChildren = DerNode.getSequence(pkcs8Children, 1).getChildren();
oidString = algorithmIdChildren[0].toVal();
algorithmParameters = algorithmIdChildren[1];
// Get the value of the 3rd child which is the octet string.
privateKeyDer = pkcs8Children[2].toVal();
} catch (ex) {
// Error decoding as PKCS #8. Try PKCS #1 for backwards compatibility.
try {
this.loadPkcs1(encoding);
return;
} catch (ex) {
throw new TpmPrivateKey.Error(new Error
("loadPkcs8: Error decoding private key: " + ex));
}
}
if (oidString == TpmPrivateKey.EC_ENCRYPTION_OID)
keyType = KeyType.EC;
else if (oidString == TpmPrivateKey.RSA_ENCRYPTION_OID)
keyType = KeyType.RSA;
else
throw new TpmPrivateKey.Error(new Error
("loadPkcs8: Unrecognized private key OID: " + oidString));
}
this.loadPkcs1(privateKeyDer, keyType);
};
/**
* Get the encoded public key for this private key.
* @return {Blob} The public key encoding Blob.
* @throws TpmPrivateKey.Error if no private key is loaded, or error converting
* to a public key.
*/
TpmPrivateKey.prototype.derivePublicKey = function()
{
if (this.keyType_ != KeyType.RSA)
throw new TpmPrivateKey.Error(new Error
("derivePublicKey: The private key is not loaded"));
try {
var privateKeyBase64 = this.privateKey_.toString().replace
("-----BEGIN RSA PRIVATE KEY-----", "").replace
("-----END RSA PRIVATE KEY-----", "");
var rsaPrivateKeyDer = new Buffer(privateKeyBase64, 'base64');
// Decode the PKCS #1 RSAPrivateKey.
var parsedNode = DerNode.parse(rsaPrivateKeyDer, 0);
var rsaPrivateKeyChildren = parsedNode.getChildren();
var modulus = rsaPrivateKeyChildren[1];
var publicExponent = rsaPrivateKeyChildren[2];
// Encode the PKCS #1 RSAPublicKey.
var rsaPublicKey = new DerNode.DerSequence();
rsaPublicKey.addChild(modulus);
rsaPublicKey.addChild(publicExponent);
var rsaPublicKeyDer = rsaPublicKey.encode();
// Encode the SubjectPublicKeyInfo.
var algorithmIdentifier = new DerNode.DerSequence();
algorithmIdentifier.addChild(new DerNode.DerOid(new OID
(TpmPrivateKey.RSA_ENCRYPTION_OID)));
algorithmIdentifier.addChild(new DerNode.DerNull());
var publicKey = new DerNode.DerSequence();
publicKey.addChild(algorithmIdentifier);
publicKey.addChild(new DerNode.DerBitString(rsaPublicKeyDer.buf(), 0));
return publicKey.encode();
} catch (ex) {
// We don't expect this to happen since the key was encoded here.
throw new TpmPrivateKey.Error(new Error
("derivePublicKey: Error decoding private key " + ex));
}
};
/**
* Decrypt the cipherText using this private key according the encryption
* algorithmType. Only RSA encryption is supported for now.
* @param {Buffer} cipherText The cipher text byte buffer.
* @param {number} algorithmType (optional) This decrypts according to
* algorithmType which is an int from the EncryptAlgorithmType enum. If omitted,
* use RsaOaep.
* @param {boolean} useSync (optional) If true then return a SyncPromise which
* is already fulfilled. If omitted or false, this may return a SyncPromise or
* an async Promise.
* @return {Promise|SyncPromise} A promise which returns the decrypted data Blob,
* or a promise rejected with TpmPrivateKey.Error if the private key is not
* loaded, if decryption is not supported for this key type, or for error
* decrypting.
*/
TpmPrivateKey.prototype.decryptPromise = function
(cipherText, algorithmType, useSync)
{
if (algorithmType == undefined)
algorithmType = EncryptAlgorithmType.RsaOaep;
if (this.keyType_ == null)
return SyncPromise.reject(new TpmPrivateKey.Error(new Error
("decrypt: The private key is not loaded")));
// TODO: Check for UseSubtleCrypto.
var padding;
if (algorithmType == EncryptAlgorithmType.RsaPkcs)
padding = constants.RSA_PKCS1_PADDING;
else if (algorithmType == EncryptAlgorithmType.RsaOaep)
padding = constants.RSA_PKCS1_OAEP_PADDING;
else
return SyncPromise.reject(new TpmPrivateKey.Error(new Error
("unsupported padding scheme")));
try {
// In Node.js, privateDecrypt requires version v0.12.
return SyncPromise.resolve(new Blob
(Crypto.privateDecrypt
({ key: this.privateKey_, padding: padding }, cipherText),
false));
} catch (err) {
return SyncPromise.reject(new TpmPrivateKey.Error(err));
}
};
/**
* Sign the data with this private key, returning a signature Blob.
* @param {Buffer} data The input byte buffer.
* @param {number} digestAlgorithm The digest algorithm as an int from the
* DigestAlgorithm enum.
* @param {boolean} useSync (optional) If true then return a SyncPromise which
* is already fulfilled. If omitted or false, this may return a SyncPromise or
* an async Promise.
* @return {Promise|SyncPromise} A promise which returns the signature Blob (or
* an isNull Blob if this private key is not initialized), or a promise rejected
* with TpmPrivateKey.Error for unrecognized digestAlgorithm or an error in
* signing.
*/
TpmPrivateKey.prototype.signPromise = function(data, digestAlgorithm, useSync)
{
if (this.keyType_ == null)
return SyncPromise.reject(new TpmPrivateKey.Error(new Error
("sign: The private key is not loaded")));
if (digestAlgorithm != DigestAlgorithm.SHA256)
return SyncPromise.reject(new TpmPrivateKey.Error(new Error
("TpmPrivateKey.sign: Unsupported digest algorithm")));
if (UseSubtleCrypto() && !useSync) {
var algo = {name:"RSASSA-PKCS1-v1_5", hash:{name:"SHA-256"}};
var promise;
if (!this.privateKey_.subtleKey) {
// This is the first time in the session that we're using crypto subtle
// with this key so we have to convert to pkcs8 and import it. Assigning
// it to this.privateKey_.subtleKey means we only have to do this once per
// session, giving us a small but not insignificant, performance boost.
var privateDER = DataUtils.privateKeyPemToDer(this.privateKey_);
var pkcs8 = TpmPrivateKey.encodePkcs8PrivateKey
(privateDER, new OID(TpmPrivateKey.RSA_ENCRYPTION_OID),
new DerNode.DerNull()).buf();
var thisKey = this;
promise = crypto.subtle.importKey
("pkcs8", pkcs8.buffer, algo, true, ["sign"])
.then(function(subtleKey) {
// Cache the crypto.subtle key object.
thisKey.privateKey_.subtleKey = subtleKey;
return crypto.subtle.sign(algo, subtleKey, data);
});
}
else
// The crypto.subtle key has been cached on a previous sign or from keygen.
promise = crypto.subtle.sign(algo, this.privateKey_.subtleKey, data);
return promise
.then(function(signature) {
var result = new Blob(new Uint8Array(signature), true);
return Promise.resolve(result);
});
}
else {
var signer;
if (this.keyType_ === KeyType.RSA)
signer = Crypto.createSign("RSA-SHA256");
else if (this.keyType === KeyType.EC)
// Just create a "sha256". The Crypto library will infer ECDSA from the key.
signer = Crypto.createSign("sha256");
else
return SyncPromise.resolve(new Blob());
signer.update(data);
var signature = new Buffer
(DataUtils.toNumbersIfString(signer.sign(this.privateKey_)));
var result = new Blob(signature, false);
return SyncPromise.resolve(result);
}
};
/**
* Get the encoded unencrypted private key in PKCS #1.
* @return {Blob} The private key encoding Blob.
* @throws {TpmPrivateKey.Error} If no private key is loaded, or error encoding.
*/
TpmPrivateKey.prototype.toPkcs1 = function()
{
if (this.keyType_ == null)
throw new TpmPrivateKey.Error(new Error
("toPkcs1: The private key is not loaded"));
// this.privateKey_ is already the base64-encoded PKCS #1 key.
var privateKeyBase64 = this.privateKey_.replace
("-----BEGIN RSA PRIVATE KEY-----", "").replace
("-----END RSA PRIVATE KEY-----", "");
return new Blob(new Buffer(privateKeyBase64, 'base64'));
};
/**
* Get the encoded unencrypted private key in PKCS #8.
* @return {Blob} The private key encoding Blob.
* @throws {TpmPrivateKey.Error} If no private key is loaded, or error encoding.
*/
TpmPrivateKey.prototype.toPkcs8 = function()
{
if (this.keyType_ == null)
throw new TpmPrivateKey.Error(new Error
("toPkcs8: The private key is not loaded"));
var oid;
if (this.keyType_ === KeyType.RSA)
oid = new OID(TpmPrivateKey.RSA_ENCRYPTION_OID);
else if (this.keyType === KeyType.EC)
oid = new OID(TpmPrivateKey.EC_ENCRYPTION_OID);
else
// We don't expect this to happen.
throw new TpmPrivateKey.Error(new Error
("toPkcs8: Unrecognized key type " + this.keyType_));
return TpmPrivateKey.encodePkcs8PrivateKey
(this.toPkcs1().buf(), oid, new DerNode.DerNull());
};
/**
* Generate a key pair according to keyParams and return a new TpmPrivateKey
* with the private key. You can get the public key with derivePublicKey.
* @param {KeyParams} keyParams The parameters of the key.
* @param {boolean} useSync (optional) If true then return a SyncPromise which
* is already fulfilled. If omitted or false, this may return a SyncPromise or
* an async Promise.
* @return {Promise|SyncPromise} A promise which returns the new TpmPrivateKey,
* or a promise rejected with Error if the key type is not supported, or a
* promise rejected with TpmPrivateKey.Error for an invalid key size, or an
* error generating.
*/
TpmPrivateKey.generatePrivateKeyPromise = function(keyParams, useSync)
{
// TODO: Check for UseSubtleCrypto.
// TODO: Check for RSAKey in the browser.
// Assume we are in Node.js.
var privateKeyPem;
if (keyParams.getKeyType() === KeyType.RSA) {
if (!rsaKeygen)
return SyncPromise.reject(new TpmPrivateKey.Error(new Error
("Need to install rsa-keygen: sudo npm install rsa-keygen")));
var keyPair = rsaKeygen.generate(keyParams.getKeySize());
privateKeyPem = keyPair.private_key.toString();
}
else
return SyncPromise.reject(new Error
("Cannot generate a key pair of type " + keyParams.getKeyType()));
var result = new TpmPrivateKey();
result.privateKey_ = privateKeyPem;
result.keyType_ = keyParams.getKeyType();
return SyncPromise.resolve(result);
};
/**
* Encode the private key to a PKCS #8 private key. We do this explicitly here
* to avoid linking to extra OpenSSL libraries.
* @param {Buffer} privateKeyDer The input private key DER.
* @param {OID} oid The OID of the privateKey.
* @param {DerNode} parameters The DerNode of the parameters for the OID.
* @return {Blob} The PKCS #8 private key DER.
*/
TpmPrivateKey.encodePkcs8PrivateKey = function(privateKeyDer, oid, parameters)
{
var algorithmIdentifier = new DerNode.DerSequence();
algorithmIdentifier.addChild(new DerNode.DerOid(oid));
algorithmIdentifier.addChild(parameters);
var result = new DerNode.DerSequence();
result.addChild(new DerNode.DerInteger(0));
result.addChild(algorithmIdentifier);
result.addChild(new DerNode.DerOctetString(privateKeyDer));
return result.encode();
};
/**
* Encode the RSAKey private key as a PKCS #1 private key.
* @param {RSAKey} rsaKey The RSAKey private key.
* @return {Blob} The PKCS #1 private key DER.
*/
TpmPrivateKey.encodePkcs1PrivateKeyFromRSAKey = function(rsaKey)
{
// Imitate KJUR getEncryptedPKCS5PEMFromRSAKey.
var result = new DerNode.DerSequence();
result.addChild(new DerNode.DerInteger(0));
result.addChild(new DerNode.DerInteger(TpmPrivateKey.bigIntegerToBuffer(rsaKey.n)));
result.addChild(new DerNode.DerInteger(rsaKey.e));
result.addChild(new DerNode.DerInteger(TpmPrivateKey.bigIntegerToBuffer(rsaKey.d)));
result.addChild(new DerNode.DerInteger(TpmPrivateKey.bigIntegerToBuffer(rsaKey.p)));
result.addChild(new DerNode.DerInteger(TpmPrivateKey.bigIntegerToBuffer(rsaKey.q)));
result.addChild(new DerNode.DerInteger(TpmPrivateKey.bigIntegerToBuffer(rsaKey.dmp1)));
result.addChild(new DerNode.DerInteger(TpmPrivateKey.bigIntegerToBuffer(rsaKey.dmq1)));
result.addChild(new DerNode.DerInteger(TpmPrivateKey.bigIntegerToBuffer(rsaKey.coeff)));
return result.encode();
};
/**
* Encode the public key values in the RSAKey private key as a
* SubjectPublicKeyInfo.
* @param {RSAKey} rsaKey The RSAKey private key with the public key values.
* @return {Blob} The SubjectPublicKeyInfo DER.
*/
TpmPrivateKey.encodePublicKeyFromRSAKey = function(rsaKey)
{
var rsaPublicKey = new DerNode.DerSequence();
rsaPublicKey.addChild(new DerNode.DerInteger
(TpmPrivateKey.bigIntegerToBuffer(rsaKey.n)));
rsaPublicKey.addChild(new DerNode.DerInteger(rsaKey.e));
var algorithmIdentifier = new DerNode.DerSequence();
algorithmIdentifier.addChild
(new DerNode.DerOid(new OID(TpmPrivateKey.RSA_ENCRYPTION_OID)));
algorithmIdentifier.addChild(new DerNode.DerNull());
var result = new DerNode.DerSequence();
result.addChild(algorithmIdentifier);
result.addChild(new DerNode.DerBitString(rsaPublicKey.encode().buf(), 0));
return result.encode();
};
/**
* Convert a BigInteger to a Buffer.
* @param {BigInteger} bigInteger The BigInteger.
* @return {Buffer} The Buffer.
*/
TpmPrivateKey.bigIntegerToBuffer = function(bigInteger)
{
// Imitate KJUR.asn1.ASN1Util.bigIntToMinTwosComplementsHex.
var hex = bigInteger.toString(16);
if (hex.substr(0, 1) == "-")
throw new Error
("TpmPrivateKey.bigIntegerToBuffer: Negative integers are not currently supported");
if (hex.length % 2 == 1)
// Odd number of characters.
hex = "0" + hex;
else {
if (! hex.match(/^[0-7]/))
// The first byte is >= 0x80, so prepend a zero to keep it positive.
hex = "00" + hex;
}
return new Buffer(hex, 'hex');
};
TpmPrivateKey.RSA_ENCRYPTION_OID = "1.2.840.113549.1.1.1";
TpmPrivateKey.EC_ENCRYPTION_OID = "1.2.840.10045.2.1";