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

/**
 * Copyright (C) 2015-2016 Regents of the University of California.
 * @author: Jeff Thompson <jefft0@remap.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.
 */

// Use capitalized Crypto to not clash with the browser's crypto.subtle.
var Crypto = require('../../crypto.js');
// Don't require other modules since this is meant for the browser, not Node.js.

/**
 * IndexedDbPrivateKeyStorage extends PrivateKeyStorage to implement private key
 * storage using the browser's IndexedDB service.
 * @constructor
 */
var IndexedDbPrivateKeyStorage = function IndexedDbPrivateKeyStorage()
{
  PrivateKeyStorage.call(this);

  this.database = new Dexie("ndnsec-tpm");
  this.database.version(1).stores({
    // "nameHash" is transformName(keyName) // string
    // "encoding" is the public key DER     // Uint8Array
    publicKey: "nameHash",

    // "nameHash" is transformName(keyName)     // string
    // "encoding" is the PKCS 8 private key DER // Uint8Array
    privateKey: "nameHash"
  });
  this.database.open();
};

IndexedDbPrivateKeyStorage.prototype = new PrivateKeyStorage();
IndexedDbPrivateKeyStorage.prototype.name = "IndexedDbPrivateKeyStorage";

/**
 * Generate a pair of asymmetric keys.
 * @param {Name} keyName The name of the key pair.
 * @param {KeyParams} params The parameters of the key.
 * @param {boolean} useSync (optional) If true then return a rejected promise
 * since this only supports async code.
 * @return {Promise} A promise that fulfills when the pair is generated.
 */
IndexedDbPrivateKeyStorage.prototype.generateKeyPairPromise = function
  (keyName, params, useSync)
{
  if (useSync)
    return Promise.reject(new SecurityException(new Error
      ("IndexedDbPrivateKeyStorage.generateKeyPairPromise is only supported for async")));

  var thisStorage = this;

  return thisStorage.doesKeyExistPromise(keyName, KeyClass.PUBLIC)
  .then(function(exists) {
    if (exists)
      throw new Error("Public key already exists");

    return thisStorage.doesKeyExistPromise(keyName, KeyClass.PRIVATE);
  })
  .then(function(exists) {
    if (exists)
      throw new Error("Private key already exists");

    if (params.getKeyType() === KeyType.RSA) {
      var privateKey = null;
      var publicKeyDer = null;

      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) {
        privateKey = key.privateKey;

        // Export the public key to DER.
        return crypto.subtle.exportKey("spki", key.publicKey);
      })
      .then(function(exportedPublicKey) {
        publicKeyDer = new Uint8Array(exportedPublicKey);

        // Export the private key to DER.
        return crypto.subtle.exportKey("pkcs8", privateKey);
      })
      .then(function(pkcs8Der) {
        // Save the key pair
        return thisStorage.database.transaction
          ("rw", thisStorage.database.privateKey, thisStorage.database.publicKey, function () {
            thisStorage.database.publicKey.put
              ({nameHash: IndexedDbPrivateKeyStorage.transformName(keyName),
                encoding: publicKeyDer});
            thisStorage.database.privateKey.put
              ({nameHash: IndexedDbPrivateKeyStorage.transformName(keyName),
                encoding: new Uint8Array(pkcs8Der)});
          });
      });
    }
    else
      throw 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.
 * @param {boolean} useSync (optional) If true then return a rejected promise
 * since this only supports async code.
 * @return {Promise} A promise that fulfills when the key pair is deleted.
 */
IndexedDbPrivateKeyStorage.prototype.deleteKeyPairPromise = function
  (keyName, useSync)
{
  if (useSync)
    return Promise.reject(new SecurityException(new Error
      ("IndexedDbPrivateKeyStorage.deleteKeyPairPromise is only supported for async")));

  var thisStorage = this;
  // delete does nothing if the key doesn't exist.
  return this.database.publicKey.delete
    (IndexedDbPrivateKeyStorage.transformName(keyName))
  .then(function() {
    return thisStorage.database.privateKey.delete
      (IndexedDbPrivateKeyStorage.transformName(keyName));
  });
};

/**
 * Get the public key
 * @param {Name} keyName The name of public key.
 * @param {boolean} useSync (optional) If true then return a rejected promise
 * since this only supports async code.
 * @return {Promise} A promise that returns the PublicKey.
 */
IndexedDbPrivateKeyStorage.prototype.getPublicKeyPromise = function
  (keyName, useSync)
{
  if (useSync)
    return Promise.reject(new SecurityException(new Error
      ("IndexedDbPrivateKeyStorage.getPublicKeyPromise is only supported for async")));

  return this.database.publicKey.get
    (IndexedDbPrivateKeyStorage.transformName(keyName))
  .then(function(publicKeyEntry) {
    return Promise.resolve(new PublicKey(new Blob(publicKeyEntry.encoding)));
  });
};

/**
 * 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 {boolean} useSync (optional) If true then return a rejected promise
 * since this only supports async code.
 * @return {Promise} A promise that returns the signature Blob.
 */
IndexedDbPrivateKeyStorage.prototype.signPromise = function
  (data, keyName, digestAlgorithm, useSync)
{
  useSync = (typeof digestAlgorithm === "boolean") ? digestAlgorithm : useSync;
  digestAlgorithm = (typeof digestAlgorithm === "boolean" || !digestAlgorithm) ? DigestAlgorithm.SHA256 : digestAlgorithm;

  if (useSync)
    return Promise.reject(new SecurityException(new Error
      ("IndexedDbPrivateKeyStorage.signPromise is only supported for async")));

  if (digestAlgorithm != DigestAlgorithm.SHA256)
    return Promise.reject(new SecurityException(new Error
      ("IndexedDbPrivateKeyStorage.sign: Unsupported digest algorithm")));

  // TODO: Support non-RSA keys.
  var algo = { name: "RSASSA-PKCS1-v1_5", hash: {name: "SHA-256" }};

  // Find the private key.
  return this.database.privateKey.get
    (IndexedDbPrivateKeyStorage.transformName(keyName))
  .then(function(privateKeyEntry) {
    return crypto.subtle.importKey
      ("pkcs8", new Blob(privateKeyEntry.encoding).buf(), algo, true, ["sign"]);
  })
  .then(function(privateKey) {
    return crypto.subtle.sign(algo, privateKey, data);
  })
  .then(function(signature) {
    return Promise.resolve(new Blob(new Uint8Array(signature), true));
  });
};

/**
 * 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.
 * @param {boolean} useSync (optional) If true then return a rejected promise
 * since this only supports async code.
 * @return {Promise} A promise which returns true if the key exists.
 */
IndexedDbPrivateKeyStorage.prototype.doesKeyExistPromise = function
  (keyName, keyClass, useSync)
{
  if (useSync)
    return Promise.reject(new SecurityException(new Error
      ("IndexedDbPrivateKeyStorage.doesKeyExistPromise is only supported for async")));

  var table = null;
  if (keyClass == KeyClass.PUBLIC)
    table = this.database.publicKey;
  else if (keyClass == KeyClass.PRIVATE)
    table = this.database.privateKey;
  else
    // Silently say that anything else doesn't exist.
    return Promise.resolve(false);

  return table.where("nameHash").equals
    (IndexedDbPrivateKeyStorage.transformName(keyName))
  .count()
  .then(function(count) {
    return Promise.resolve(count > 0);
  });
};

/**
 * Transform the key name into the base64 encoding of the hash (the same as in
 * FilePrivateKeyStorage without the file name extension).
 */
IndexedDbPrivateKeyStorage.transformName = function(keyName)
{
  var hash = Crypto.createHash('sha256');
  hash.update(new Buffer(keyName.toUri()));
  var fileName = hash.digest('base64');
  return fileName.replace(/\//g, '%');
};