/**
* Copyright (C) 2014-2015 Regents of the University of California.
* @author: Jeff Thompson <[email protected]>
* @author: Andrew Brown <[email protected]>
* From ndn-cxx security by Yingdi Yu <[email protected]>.
*
* 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.
*/
var KeyClass = require('../security-types').KeyClass;
var KeyType = require('../security-types').KeyType;
var DigestAlgorithm = require('../security-types').DigestAlgorithm;
var SecurityException = require('../security-exception').SecurityException;
var PublicKey = require('../certificate/public-key').PublicKey;
var PrivateKeyStorage = require('./private-key-storage').PrivateKeyStorage;
var Blob = require('../../util/blob').Blob;
var OID = require('../../encoding/oid').OID;
var DerNode = require('../../encoding/der/der-node').DerNode;
var DataUtils = require('../../encoding/data-utils.js').DataUtils;
var util = require('util');
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var rsaKeygen = null;
try {
// This should be installed with: sudo npm install rsa-keygen
rsaKeygen = require('rsa-keygen');
}
catch (e) {}
/**
* FilePrivateKeyStorage works with NFD's default private key storage, the files
* stored in .ndn/ndnsec-tpm-file. This library will not be available from the
* browser
* @param {string} nonDefaultTpmPath if desired, override the default TPM path (i.e. .ndn/ndnsec-tpm-file)
* @constructor
*/
var FilePrivateKeyStorage = function FilePrivateKeyStorage(nonDefaultTpmPath)
{
PrivateKeyStorage.call(this);
// Path to TPM folder.
this.tpmPath = nonDefaultTpmPath ||
path.join(FilePrivateKeyStorage.getUserHomePath(), '.ndn', 'ndnsec-tpm-file');
};
util.inherits(FilePrivateKeyStorage, PrivateKeyStorage);
exports.FilePrivateKeyStorage = FilePrivateKeyStorage;
/**
* Check if a particular key exists.
* @param {Name} keyName The name of the key.
* @param {number} keyClass The class of the key, e.g. KeyClass.PUBLIC,
* KeyClass.PRIVATE, or KeyClass.SYMMETRIC.
* @returns {boolean} True if the key exists, otherwise false.
*/
FilePrivateKeyStorage.prototype.doesKeyExist = function (keyName, keyClass)
{
return fs.existsSync(this.transformName(keyName, keyClass));
};
/**
* Generate a pair of asymmetric keys; only currently supports RSA
* @param {Name} keyName The name of the key pair.
* @param {KeyParams} params (optional) The parameters of the key.
*/
FilePrivateKeyStorage.prototype.generateKeyPair = function (keyName, params)
{
if (this.doesKeyExist(keyName, KeyClass.PUBLIC)) {
throw new SecurityException(new Error("Public key already exists"));
}
if (this.doesKeyExist(keyName, KeyClass.PRIVATE)) {
throw new SecurityException(new Error("Public key already exists"));
}
// build keys
if (params.getKeyType() === KeyType.RSA) {
if (!rsaKeygen)
throw new SecurityException(new Error
("Need to install rsa-keygen: sudo npm install rsa-keygen"));
var keyPair = rsaKeygen.generate(params.getKeySize());
// Get the public key DER from the PEM string.
var publicKeyBase64 = keyPair.public_key.toString().replace
("-----BEGIN PUBLIC KEY-----", "").replace
("-----END PUBLIC KEY-----", "");
var publicKey = new Buffer(publicKeyBase64, 'base64');
// Get the PKCS1 private key DER from the PEM string and encode as PKCS8.
var privateKeyBase64 = keyPair.private_key.toString().replace
("-----BEGIN RSA PRIVATE KEY-----", "").replace
("-----END RSA PRIVATE KEY-----", "");
var pkcs1PrivateKeyDer = new Buffer(privateKeyBase64, 'base64');
var privateKey = FilePrivateKeyStorage.encodePkcs8PrivateKey
(pkcs1PrivateKeyDer, new OID(FilePrivateKeyStorage.RSA_ENCRYPTION_OID),
new DerNode.DerNull()).buf();
// save
this.write(keyName, KeyClass.PRIVATE, privateKey);
this.write(keyName, KeyClass.PUBLIC, publicKey);
}
else {
throw new SecurityException(new Error("Only RSA key generation currently supported"));
}
};
/**
* Delete a pair of asymmetric keys. If the key doesn't exist, do nothing.
* @param {Name} keyName The name of the key pair.
*/
FilePrivateKeyStorage.prototype.deleteKeyPair = function (keyName)
{
this.deleteKey(keyName);
};
/**
* Delete all keys with this name. If the key doesn't exist, do nothing.
* @param {Name} keyName The name of the key pair.
*/
FilePrivateKeyStorage.prototype.deleteKey = function (keyName)
{
for (var keyClassName in KeyClass) {
var keyClass = KeyClass[keyClassName];
if (this.doesKeyExist(keyName, keyClass)) {
var filePath = this.transformName(keyName, keyClass);
fs.unlinkSync(filePath);
}
}
};
/**
* Get the public key
* @param {Name} keyName The name of public key.
* @returns {PublicKey} The public key.
*/
FilePrivateKeyStorage.prototype.getPublicKey = function (keyName)
{
var buffer = this.read(keyName, KeyClass.PUBLIC);
return new PublicKey(new Blob(buffer));
};
/**
* Fetch the private key for keyName and sign the data to produce a signature Blob.
* @param {Buffer} data Pointer to the input byte array.
* @param {Name} keyName The name of the signing key.
* @param {number} digestAlgorithm (optional) The digest algorithm from
* DigestAlgorithm, such as DigestAlgorithm.SHA256. If omitted, use
* DigestAlgorithm.SHA256.
* @param {function} onComplete (optional) This calls onComplete(signature) with
* the signature Blob. If omitted, the return value is the signature Blob. (Some
* crypto libraries only use a callback, so onComplete is required to use these.)
* @returns {Blob} If onComplete is omitted, return the signature Blob. Otherwise,
* return null and use onComplete as described above.
*/
FilePrivateKeyStorage.prototype.sign = function
(data, keyName, digestAlgorithm, onComplete)
{
if (digestAlgorithm == null)
digestAlgorithm = DigestAlgorithm.SHA256;
if (!this.doesKeyExist(keyName, KeyClass.PRIVATE))
throw new SecurityException(new Error
("FilePrivateKeyStorage.sign: private key doesn't exist"));
if (digestAlgorithm != DigestAlgorithm.SHA256)
throw new SecurityException(new Error
("FilePrivateKeyStorage.sign: Unsupported digest algorithm"));
// Retrieve the private key.
var keyType = [-1];
var privateKey = this.getPrivateKey(keyName, keyType);
// Sign.
if (keyType[0] == KeyType.RSA) {
var rsa = require("crypto").createSign('RSA-SHA256');
rsa.update(data);
var signature = new Buffer(DataUtils.toNumbersIfString(rsa.sign(privateKey)));
var result = new Blob(signature, false);
if (onComplete) {
onComplete(result);
return null;
}
else
return result;
}
else
// We don't expect this to happen since getPrivateKey checked it.
throw new SecurityException(new Error
("FilePrivateKeyStorage: Unsupported signature key type " + keyType[0]));
};
/** PRIVATE METHODS **/
/**
* A private method to get the private key.
* @param {Name} keyName The name of private key.
* @param {Array<KeyType>} keyType Set keyType[0] to the KeyType.
* @returns {string} The PEM-encoded private key for use by the crypto module.
*/
FilePrivateKeyStorage.prototype.getPrivateKey = function(keyName, keyType)
{
var pkcs8Der = this.read(keyName, KeyClass.PRIVATE);
// The private key is generated by NFD which stores as PKCS #8. Decode it
// to find the algorithm OID and the inner private key DER.
var parsedNode = DerNode.parse(pkcs8Der);
var pkcs8Children = parsedNode.getChildren();
// Get the algorithm OID and parameters.
var algorithmIdChildren = DerNode.getSequence(pkcs8Children, 1).getChildren();
var oidString = algorithmIdChildren[0].toVal();
var algorithmParameters = algorithmIdChildren[1];
// Get the value of the 3rd child which is the octet string.
var privateKeyDer = pkcs8Children[2].toVal();
if (oidString == FilePrivateKeyStorage.RSA_ENCRYPTION_OID) {
keyType[0] = KeyType.RSA;
// Encode the DER as PEM.
var keyBase64 = privateKeyDer.buf().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-----";
return keyPem;
}
else
throw new SecurityException(new Error
("FilePrivateKeyStorage::sign: Unrecognized private key OID: " + oidString));
};
/**
* 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.
*/
FilePrivateKeyStorage.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();
};
FilePrivateKeyStorage.RSA_ENCRYPTION_OID = "1.2.840.113549.1.1.1";
FilePrivateKeyStorage.EC_ENCRYPTION_OID = "1.2.840.10045.2.1";
/**
* File extensions by KeyClass type
*/
var KeyClassExtensions = {};
KeyClassExtensions[KeyClass.PUBLIC] = '.pub';
KeyClassExtensions[KeyClass.PRIVATE] = '.pri';
KeyClassExtensions[KeyClass.SYMMETRIC] = '.key';
/**
* Write to a key file
* @param {Name} keyName
* @param {KeyClass} keyClass [PUBLIC, PRIVATE, SYMMETRIC]
* @param {Buffer} bytes
* @throws Error if the file cannot be written to
*/
FilePrivateKeyStorage.prototype.write = function(keyName, keyClass, bytes) {
var options = { mode: parseInt('0400', 8) };
if(keyClass === KeyClass.PUBLIC) options.mode = parseInt('0444', 8);
fs.writeFileSync(this.transformName(keyName, keyClass), bytes.toString('base64'), options);
};
/**
* Read from a key file
* @param keyName
* @param keyClass [PUBLIC, PRIVATE, SYMMETRIC]
* @return {Buffer} key bytes
* @throws Error if the file cannot be read from
*/
FilePrivateKeyStorage.prototype.read = function(keyName, keyClass){
var base64 = fs.readFileSync(this.transformName(keyName, keyClass)).toString();
return new Buffer(base64, 'base64');
};
/**
* Retrieve the user's current home directory
* @returns {string} path to the user's home directory
*/
FilePrivateKeyStorage.getUserHomePath = function() {
return process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
};
/**
* Transform the key name into a file name
* @param {Name} keyName
* @param {KeyClass} keyClass
*/
FilePrivateKeyStorage.prototype.transformName = function(keyName, keyClass) {
var hash = crypto.createHash('sha256');
if (!hash) {
throw new SecurityException(new Error('Could not instantiate SHA256 hash algorith.'));
}
// hash the key name
hash.update(new Buffer(keyName.toUri()));
var fileName = hash.digest('base64');
if (!fileName) {
throw new SecurityException(new Error('Failed to hash file name: ' + keyName.toUri()));
}
// return
return path.join(this.tpmPath, fileName.replace(/\//g, '%') + KeyClassExtensions[keyClass]);
};