Source: security/identity/file-private-key-storage.js

/**
 * Copyright (C) 2014-2018 Regents of the University of California.
 * @author: Jeff Thompson <jefft0@remap.ucla.edu>
 * @author: Andrew Brown <andrew.brown@intel.com>
 * From ndn-cxx security by Yingdi Yu <yingdi@cs.ucla.edu>.
 *
 * 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.
 */

/** @ignore */
var KeyClass = require('../security-types').KeyClass; /** @ignore */
var KeyType = require('../security-types').KeyType; /** @ignore */
var DigestAlgorithm = require('../security-types').DigestAlgorithm; /** @ignore */
var SecurityException = require('../security-exception.js').SecurityException; /** @ignore */
var PublicKey = require('../certificate/public-key.js').PublicKey; /** @ignore */
var PrivateKeyStorage = require('./private-key-storage.js').PrivateKeyStorage; /** @ignore */
var Blob = require('../../util/blob.js').Blob; /** @ignore */
var OID = require('../../encoding/oid.js').OID; /** @ignore */
var DerNode = require('../../encoding/der/der-node.js').DerNode; /** @ignore */
var DataUtils = require('../../encoding/data-utils.js').DataUtils; /** @ignore */
var SyncPromise = require('../../util/sync-promise.js').SyncPromise; /** @ignore */
var util = require('util');
// Use capitalized Crypto to not clash with the browser's crypto.subtle.
/** @ignore */
var Crypto = require('../../crypto.js'); /** @ignore */
var fs = require('fs'); /** @ignore */
var path = require('path'); /** @ignore */
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.
 * @return {SyncPromise} A promise which returns true if the key exists.
 */
FilePrivateKeyStorage.prototype.doesKeyExistPromise = function(keyName, keyClass)
{
  var exists = fs.existsSync
    (this.nameTransform(keyName.toUri(), KeyClassExtensions[keyClass]));
  return SyncPromise.resolve(exists);
};

/**
 * Generate a pair of asymmetric keys; only currently supports RSA
 * @param {Name} keyName The name of the key pair.
 * @param {KeyParams} params The parameters of the key.
 * @return {SyncPromise} A promise that fulfills when the pair is generated.
 */
FilePrivateKeyStorage.prototype.generateKeyPairPromise = function
  (keyName, params)
{
  if (this.doesKeyExist(keyName, KeyClass.PUBLIC)) {
    return SyncPromise.reject(new SecurityException(new Error
      ("Public key already exists")));
  }
  if (this.doesKeyExist(keyName, KeyClass.PRIVATE)) {
    return SyncPromise.reject(new SecurityException(new Error
      ("Private key already exists")));
  }

  // build keys
  if (params.getKeyType() === KeyType.RSA) {
    if (!rsaKeygen)
      return SyncPromise.reject(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 = PrivateKeyStorage.encodePkcs8PrivateKey
      (pkcs1PrivateKeyDer, new OID(PrivateKeyStorage.RSA_ENCRYPTION_OID),
       new DerNode.DerNull()).buf();

    // save
    this.write(keyName, KeyClass.PRIVATE, privateKey);
    this.write(keyName, KeyClass.PUBLIC, publicKey);
  }
  else {
    return SyncPromise.reject(new SecurityException(new Error
      ("Only RSA key generation currently supported")));
  }

  return SyncPromise.resolve();
};

/**
 * Delete a pair of asymmetric keys. If the key doesn't exist, do nothing.
 * @param {Name} keyName The name of the key pair.
 * @return {SyncPromise} A promise that fulfills when the key pair is deleted.
 */
FilePrivateKeyStorage.prototype.deleteKeyPairPromise = function (keyName)
{
  this.deleteKey(keyName);
  return SyncPromise.resolve();
};

/**
 * 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.nameTransform(keyName.toUri(), KeyClassExtensions[keyClass]);
      fs.unlinkSync(filePath);
    }
  }
};

/**
 * Get the public key
 * @param {Name} keyName The name of public key.
 * @return {SyncPromise} A promise that returns the PublicKey.
 */
FilePrivateKeyStorage.prototype.getPublicKeyPromise = function (keyName)
{
  var buffer = this.read(keyName, KeyClass.PUBLIC);
  return SyncPromise.resolve(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.
 * @return {SyncPromise} A promise that returns the signature Blob.
 */
FilePrivateKeyStorage.prototype.signPromise = function
  (data, keyName, digestAlgorithm)
{
  if (digestAlgorithm == null)
    digestAlgorithm = DigestAlgorithm.SHA256;

  if (!this.doesKeyExist(keyName, KeyClass.PRIVATE))
    return SyncPromise.reject(new SecurityException(new Error
      ("FilePrivateKeyStorage.sign: private key doesn't exist")));

  if (digestAlgorithm != DigestAlgorithm.SHA256)
    return SyncPromise.reject(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 = Crypto.createSign('RSA-SHA256');
    rsa.update(data);

    var signature = new Buffer(DataUtils.toNumbersIfString(rsa.sign(privateKey)));
    return SyncPromise.resolve(new Blob(signature, false));
  }
  else
    // We don't expect this to happen since getPrivateKey checked it.
    return SyncPromise.reject(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.
 * @return {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 == PrivateKeyStorage.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));
};

/**
 * File extensions by KeyClass type
 */
var KeyClassExtensions = {};
KeyClassExtensions[KeyClass.PUBLIC] = '.pub';
KeyClassExtensions[KeyClass.PRIVATE] = '.pri';
KeyClassExtensions[KeyClass.SYMMETRIC] = '.key';

/**
 * Write to a key file. If keyClass is PRIVATE, then also update mapping.txt.
 * @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);

  var filePath;
  if (keyClass == KeyClass.PRIVATE)
    filePath = this.maintainMapping(keyName.toUri()) + ".pri";
  else
    filePath = this.nameTransform(keyName.toUri(), KeyClassExtensions[keyClass]);
  fs.writeFileSync(filePath, bytes.toString('base64'), options);
};

/**
 * Read from a key file
 * @param {Name} keyName
 * @param {number} keyClass An int from KeyClass.
 * @return {Buffer} key bytes
 * @throws Error if the file cannot be read from
 */
FilePrivateKeyStorage.prototype.read = function(keyName, keyClass){
  var base64 = fs.readFileSync
    (this.nameTransform(keyName.toUri(), KeyClassExtensions[keyClass])).toString();
  return new Buffer(base64, 'base64');
};

/**
 * Retrieve the user's current home directory
 * @return {string} path to the user's home directory
 */
FilePrivateKeyStorage.getUserHomePath = function() {
  return process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
};

/**
 * Transform a key name to its hashed file path.
 * @param {string} keyNameUri The key name URI which is transformed to a file path.
 * @param {string} extension The file name extension. You can use
 * KeyClassExtensions[keyClass].
 * @return {string} The hashed key file path.
 */
FilePrivateKeyStorage.prototype.nameTransform = function(keyNameUri, extension)
{
  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(keyNameUri));
  var fileName = hash.digest('base64');
  if (!fileName) {
    throw new SecurityException(new Error('Failed to hash file name: ' + keyNameUri));
  }

  // return
  return path.join(this.tpmPath, fileName.replace(/\//g, '%') + extension);
};

/**
 * Use nameTransform to get the file path for keyName (without the extension)
 * and also add to the mapping.txt file.
 * @param {string} keyNameUri The key name URI which is transformed to a file path.
 * @return {string} The key file path without the extension.
 */
FilePrivateKeyStorage.prototype.maintainMapping = function(keyNameUri)
{
  var keyFilePathNoExtension = this.nameTransform(keyNameUri, "");

  var mappingFilePath = path.join(this.tpmPath, "mapping.txt");
  fs.appendFileSync
    (mappingFilePath, keyNameUri + ' ' + keyFilePathNoExtension + '\n');

  return keyFilePathNoExtension;
};