Source: encrypt/algo/rsa-algorithm.js

/**
 * Copyright (C) 2015-2016 Regents of the University of California.
 * @author: Jeff Thompson <jefft0@remap.ucla.edu>
 * @author: From ndn-group-encrypt src/algo/rsa https://github.com/named-data/ndn-group-encrypt
 *
 * 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.
 */

// (This is ported from ndn::gep::algo::Rsa, and named RsaAlgorithm because
// "Rsa" is very short and not all the Common Client Libraries have namespaces.)

/** @ignore */
var constants = require('constants'); /** @ignore */
var Crypto = require('../../crypto.js'); /** @ignore */
var Blob = require('../../util/blob.js').Blob; /** @ignore */
var DecryptKey = require('../decrypt-key.js').DecryptKey; /** @ignore */
var EncryptKey = require('../encrypt-key.js').EncryptKey; /** @ignore */
var EncryptAlgorithmType = require('./encrypt-params.js').EncryptAlgorithmType; /** @ignore */
var DerNode = require('../../encoding/der/der-node.js').DerNode; /** @ignore */
var OID = require('../../encoding/oid.js').OID; /** @ignore */
var PrivateKeyStorage = require('../../security/identity/private-key-storage.js').PrivateKeyStorage; /** @ignore */
var UseSubtleCrypto = require('../../use-subtle-crypto-node.js').UseSubtleCrypto; /** @ignore */
var SyncPromise = require('../../util/sync-promise.js').SyncPromise; /** @ignore */
var rsaKeygen = null;
try {
  // This should be installed with: sudo npm install rsa-keygen
  rsaKeygen = require('rsa-keygen');
}
catch (e) {}

/**
 * The RsaAlgorithm class provides static methods to manipulate keys, encrypt
 * and decrypt using RSA.
 * @note This class is an experimental feature. The API may change.
 * @constructor
 */
var RsaAlgorithm = function RsaAlgorithm()
{
};

exports.RsaAlgorithm = RsaAlgorithm;

/**
 * Generate a new random decrypt key for RSA based on the given params.
 * @param {RsaKeyParams} params The key params with the key size (in bits).
 * @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 DecryptKey
 * (containing a PKCS8-encoded private key).
 */
RsaAlgorithm.generateKeyPromise = function(params, useSync)
{
  if (UseSubtleCrypto() && !useSync) {
    return crypto.subtle.generateKey
      ({ name: "RSASSA-PKCS1-v1_5", modulusLength: params.getKeySize(),
         publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
         hash: {name: "SHA-256"} },
       true, ["sign", "verify"])
    .then(function(key) {
      // Export the private key to DER.
      return crypto.subtle.exportKey("pkcs8", key.privateKey);
    })
    .then(function(pkcs8Der) {
      return Promise.resolve(new DecryptKey
        (new Blob(new Uint8Array(pkcs8Der), false)));
    });
  }
  else {
    if (!rsaKeygen)
      return SyncPromise.reject(new Error
        ("Need to install rsa-keygen: sudo npm install rsa-keygen"));

    try {
      var keyPair = rsaKeygen.generate(params.getKeySize());
      // 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();

      return SyncPromise.resolve(new DecryptKey(privateKey));
    } catch (err) {
      return SyncPromise.reject(err);
    }
  }
};

/**
 * Generate a new random decrypt key for RSA based on the given params.
 * @param {RsaKeyParams} params The key params with the key size (in bits).
 * @return {DecryptKey} The new decrypt key (containing a PKCS8-encoded private
 * key).
 * @throws Error If generateKeyPromise doesn't return a SyncPromise which is
 * already fulfilled.
 */
RsaAlgorithm.generateKey = function(params)
{
  return SyncPromise.getValue(this.generateKeyPromise(params, true));
};

/**
 * Derive a new encrypt key from the given decrypt key value.
 * @param {Blob} keyBits The key value of the decrypt key (PKCS8-encoded private
 * key).
 * @return {EncryptKey} The new encrypt key (DER-encoded public key).
 */
RsaAlgorithm.deriveEncryptKey = function(keyBits)
{
  var rsaPrivateKeyDer = RsaAlgorithm.getRsaPrivateKeyDer(keyBits);

  // Decode the PKCS #1 RSAPrivateKey.
  parsedNode = DerNode.parse(rsaPrivateKeyDer.buf(), 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
    (PrivateKeyStorage.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 new EncryptKey(publicKey.encode());
};

/**
 * Decrypt the encryptedData using the keyBits according the encrypt params.
 * @param {Blob} keyBits The key value (PKCS8-encoded private key).
 * @param {Blob} encryptedData The data to decrypt.
 * @param {EncryptParams} params This decrypts according to
 * params.getAlgorithmType().
 * @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 Blob.
 */
RsaAlgorithm.decryptPromise = function(keyBits, encryptedData, params, useSync)
{
  if (UseSubtleCrypto() && !useSync &&
      // Crypto.subtle doesn't implement PKCS1 padding.
      params.getAlgorithmType() != EncryptAlgorithmType.RsaPkcs) {
    if (params.getAlgorithmType() == EncryptAlgorithmType.RsaOaep) {
      return crypto.subtle.importKey
        ("pkcs8", keyBits.buf(), { name: "RSA-OAEP", hash: {name: "SHA-1"} },
         false, ["decrypt"])
      .then(function(privateKey) {
        return crypto.subtle.decrypt
          ({ name: "RSA-OAEP" }, privateKey, encryptedData.buf());
      })
      .then(function(result) {
        return Promise.resolve(new Blob(new Uint8Array(result), false));
      });
    }
    else
      return Promise.reject(new Error("unsupported padding scheme"));
  }
  else {
    // keyBits is PKCS #8 but we need the inner RSAPrivateKey.
    var rsaPrivateKeyDer = RsaAlgorithm.getRsaPrivateKeyDer(keyBits);

    // Encode the key DER as a PEM private key as needed by Crypto.
    var keyBase64 = rsaPrivateKeyDer.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-----";

    var padding;
    if (params.getAlgorithmType() == EncryptAlgorithmType.RsaPkcs)
      padding = constants.RSA_PKCS1_PADDING;
    else if (params.getAlgorithmType() == EncryptAlgorithmType.RsaOaep)
      padding = constants.RSA_PKCS1_OAEP_PADDING;
    else
      return SyncPromise.reject(new Error("unsupported padding scheme"));

    try {
      // In Node.js, privateDecrypt requires version v0.12.
      return SyncPromise.resolve(new Blob
        (Crypto.privateDecrypt({ key: keyPem, padding: padding }, encryptedData.buf()),
         false));
    } catch (err) {
      return SyncPromise.reject(err);
    }
  }
};

/**
 * Decrypt the encryptedData using the keyBits according the encrypt params.
 * @param {Blob} keyBits The key value (PKCS8-encoded private key).
 * @param {Blob} encryptedData The data to decrypt.
 * @param {EncryptParams} params This decrypts according to
 * params.getAlgorithmType().
 * @return {Blob} The decrypted data.
 * @throws Error If decryptPromise doesn't return a SyncPromise which is
 * already fulfilled.
 */
RsaAlgorithm.decrypt = function(keyBits, encryptedData, params)
{
  return SyncPromise.getValue(this.decryptPromise
    (keyBits, encryptedData, params, true));
};

/**
 * Encrypt the plainData using the keyBits according the encrypt params.
 * @param {Blob} keyBits The key value (DER-encoded public key).
 * @param {Blob} plainData The data to encrypt.
 * @param {EncryptParams} params This encrypts according to
 * params.getAlgorithmType().
 * @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 encrypted Blob.
 */
RsaAlgorithm.encryptPromise = function(keyBits, plainData, params, useSync)
{
  if (UseSubtleCrypto() && !useSync &&
      // Crypto.subtle doesn't implement PKCS1 padding.
      params.getAlgorithmType() != EncryptAlgorithmType.RsaPkcs) {
    if (params.getAlgorithmType() == EncryptAlgorithmType.RsaOaep) {
      return crypto.subtle.importKey
        ("spki", keyBits.buf(), { name: "RSA-OAEP", hash: {name: "SHA-1"} },
         false, ["encrypt"])
      .then(function(publicKey) {
        return crypto.subtle.encrypt
          ({ name: "RSA-OAEP" }, publicKey, plainData.buf());
      })
      .then(function(result) {
        return Promise.resolve(new Blob(new Uint8Array(result), false));
      });
    }
    else
      return Promise.reject(new Error("unsupported padding scheme"));
  }
  else {
    // Encode the key DER as a PEM public key as needed by Crypto.
    var keyBase64 = keyBits.buf().toString('base64');
    var keyPem = "-----BEGIN PUBLIC KEY-----\n";
    for (var i = 0; i < keyBase64.length; i += 64)
      keyPem += (keyBase64.substr(i, 64) + "\n");
    keyPem += "-----END PUBLIC KEY-----";

    var padding;
    if (params.getAlgorithmType() == EncryptAlgorithmType.RsaPkcs)
      padding = constants.RSA_PKCS1_PADDING;
    else if (params.getAlgorithmType() == EncryptAlgorithmType.RsaOaep)
      padding = constants.RSA_PKCS1_OAEP_PADDING;
    else
      return SyncPromise.reject(new Error("unsupported padding scheme"));

    try {
      // In Node.js, publicEncrypt requires version v0.12.
      return SyncPromise.resolve(new Blob
        (Crypto.publicEncrypt({ key: keyPem, padding: padding }, plainData.buf()),
         false));
    } catch (err) {
      return SyncPromise.reject(err);
    }
  }
};

/**
 * Encrypt the plainData using the keyBits according the encrypt params.
 * @param {Blob} keyBits The key value (DER-encoded public key).
 * @param {Blob} plainData The data to encrypt.
 * @param {EncryptParams} params This encrypts according to
 * params.getAlgorithmType().
 * @return {Blob} The encrypted data.
 * @throws Error If encryptPromise doesn't return a SyncPromise which is
 * already fulfilled.
 */
RsaAlgorithm.encrypt = function(keyBits, plainData, params)
{
  return SyncPromise.getValue(this.encryptPromise
    (keyBits, plainData, params, true));
};

/**
 * Decode the PKCS #8 private key, check that the algorithm is RSA, and return
 * the inner RSAPrivateKey DER.
 * @param {Blob} The DER-encoded PKCS #8 private key.
 * @param {Blob} The DER-encoded RSAPrivateKey.
 */
RsaAlgorithm.getRsaPrivateKeyDer = function(pkcs8PrivateKeyDer)
{
  var parsedNode = DerNode.parse(pkcs8PrivateKeyDer.buf(), 0);
  var pkcs8Children = parsedNode.getChildren();
  var algorithmIdChildren = DerNode.getSequence(pkcs8Children, 1).getChildren();
  var oidString = algorithmIdChildren[0].toVal();

  if (oidString != PrivateKeyStorage.RSA_ENCRYPTION_OID)
    throw new Error("The PKCS #8 private key is not RSA_ENCRYPTION");

  return pkcs8Children[2].getPayload();
};