UNPKG

ndn-js

Version:

A JavaScript client library for Named Data Networking

338 lines (301 loc) 12.7 kB
/** * Copyright (C) 2014-2019 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 RsaKeypair = null; try { // This should be installed with: sudo npm install rsa-keypair RsaKeypair = require('rsa-keypair'); } 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 (!RsaKeypair) return SyncPromise.reject(new SecurityException(new Error ("Need to install rsa-keypair: sudo npm install rsa-keypair"))); var keyPair = RsaKeypair.generate(params.getKeySize()); // Get the public key DER from the PEM string. var publicKeyBase64 = keyPair.publicKey.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.privateKey.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; };