ndn-js
Version:
A JavaScript client library for Named Data Networking
506 lines (454 loc) • 19 kB
JavaScript
/**
* Copyright (C) 2019 Regents of the University of California.
* @author: Jeff Thompson <jefft0@remap.ucla.edu>
* @author: From the NAC library https://github.com/named-data/name-based-access-control/blob/new/src/decryptor.cpp
*
* 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 Name = require('../name.js').Name;
var Interest = require('../interest.js').Interest;
var EncryptError = require('./encrypt-error.js').EncryptError; /** @ignore */
var KeyChain = require('../security/key-chain.js').KeyChain; /** @ignore */
var SafeBag = require('../security/safe-bag.js').SafeBag; /** @ignore */
var EncryptorV2 = require('./encryptor-v2.js').EncryptorV2; /** @ignore */
var EncryptError = require('./encrypt-error.js').EncryptError; /** @ignore */
var EncryptedContent = require('./encrypted-content.js').EncryptedContent; /** @ignore */
var EncryptParams = require('./algo/encrypt-params.js').EncryptParams; /** @ignore */
var EncryptAlgorithmType = require('./algo/encrypt-params.js').EncryptAlgorithmType; /** @ignore */
var AesAlgorithm = require('./algo/aes-algorithm.js').AesAlgorithm; /** @ignore */
var NdnCommon = require('../util/ndn-common.js').NdnCommon; /** @ignore */
var Pib = require('../security/pib/pib.js').Pib; /** @ignore */
var SyncPromise = require('../util/sync-promise.js').SyncPromise; /** @ignore */
var KeyLocatorType = require('../key-locator.js').KeyLocatorType; /** @ignore */
var LOG = require('../log.js').Log.LOG;
/**
* DecryptorV2 decrypts the supplied EncryptedContent element, using
* asynchronous operations, contingent on the retrieval of the CK Data packet,
* the KDK, and the successful decryption of both of these. For the meaning of
* "KDK", etc. see:
* https://github.com/named-data/name-based-access-control/blob/new/docs/spec.rst
*
* Create a DecryptorV2 with the given parameters.
* @param {PibKey} credentialsKey The credentials key to be used to retrieve and
* decrypt the KDK.
* @param {Validator} validator The validation policy to ensure the validity of
* the KDK and CK.
* @param {KeyChain} keyChain The KeyChain that will be used to decrypt the KDK.
* @param {Face} face The Face that will be used to fetch the CK and KDK.
* @constructor
*/
var DecryptorV2 = function DecryptorV2(credentialsKey, validator, keyChain, face)
{
// The dictionary key is the CK Name URI string. The value is a DecryptorV2.ContentKey.
// TODO: add some expiration, so they are not stored forever.
this.contentKeys_ = {};
this.credentialsKey_ = credentialsKey;
// this.validator_ = validator;
this.face_ = face;
// The external keychain with access credentials.
this.keyChain_ = keyChain;
// The internal in-memory keychain for temporarily storing KDKs.
this.internalKeyChain_ = new KeyChain("pib-memory:", "tpm-memory:");
};
exports.DecryptorV2 = DecryptorV2;
DecryptorV2.prototype.shutdown = function()
{
for (var nameUri in this.contentKeys_) {
var contentKey = this.contentKeys_[nameUri];
if (contentKey.pendingInterest > 0) {
this.face_.removePendingInterest(contentKey.pendingInterest);
contentKey.pendingInterest = 0;
for (var i in contentKey.pendingDecrypts)
contentKey.pendingDecrypts[i].onError
(EncryptError.ErrorCode.CkRetrievalFailure,
"Canceling pending decrypt as ContentKey is being destroyed");
// Clear is not really necessary, but just in case.
contentKey.pendingDecrypts = [];
}
}
};
/**
* Asynchronously decrypt the encryptedContent.
* @param {EncryptedContent} encryptedContent The EncryptedContent to decrypt,
* which must have a KeyLocator with a KEYNAME and and initial vector. This does
* not copy the EncryptedContent object. If you may change it later, then pass
* in a copy of the object.
* @param {function} onSuccess On successful decryption, this calls
* onSuccess(plainData) where plainData is the decrypted Blob.
* NOTE: The library will log any exceptions thrown by this callback, but for
* better error handling the callback should catch and properly handle any
* exceptions.
* @param {function} onError On failure, this calls onError(errorCode, message)
* where errorCode is from EncryptError.ErrorCode, and message is an error
* string.
* NOTE: The library will log any exceptions thrown by this callback, but for
* better error handling the callback should catch and properly handle any
* exceptions.
*/
DecryptorV2.prototype.decrypt = function(encryptedContent, onSuccess, onError)
{
if (encryptedContent.getKeyLocator().getType() != KeyLocatorType.KEYNAME) {
if (LOG > 3) console.log
("Missing required KeyLocator in the supplied EncryptedContent block");
onError(EncryptError.ErrorCode.MissingRequiredKeyLocator,
"Missing required KeyLocator in the supplied EncryptedContent block");
return;
}
if (!encryptedContent.hasInitialVector()) {
if (LOG > 3) console.log
("Missing required initial vector in the supplied EncryptedContent block");
onError(EncryptError.ErrorCode.MissingRequiredInitialVector,
"Missing required initial vector in the supplied EncryptedContent block");
return;
}
var ckName = encryptedContent.getKeyLocatorName();
var ckNameUri = ckName.toUri();
var contentKey = this.contentKeys_[ckNameUri];
var isNew = (contentKey === undefined);
if (isNew) {
contentKey = new DecryptorV2.ContentKey();
this.contentKeys_[ckNameUri] = contentKey;
}
if (contentKey.isRetrieved)
DecryptorV2.doDecrypt_(encryptedContent, contentKey.bits, onSuccess, onError);
else {
if (LOG > 3) console.log("CK " + ckName.toUri() +
" not yet available, so adding to the pending decrypt queue");
contentKey.pendingDecrypts.push(new DecryptorV2.ContentKey.PendingDecrypt
(encryptedContent, onSuccess, onError));
}
if (isNew)
this.fetchCk_(ckName, contentKey, onError, EncryptorV2.N_RETRIES);
};
DecryptorV2.ContentKey = function DecryptorV2ContentKey()
{
this.isRetrieved = false;
// Blob
this.bits = null;
this.pendingInterest = 0;
// Array of DecryptorV2.ContentKey.PendingDecrypt
this.pendingDecrypts = [];
};
DecryptorV2.ContentKey.PendingDecrypt = function DecryptorV2ContentKeyPendingDecrypt
(encryptedContent, onSuccess, onError)
{
// EncryptedContent
this.encryptedContent = encryptedContent;
// This calls onSuccess(plainData) where plainData is a Blob.
this.onSuccess = onSuccess;
// This calls onError(errorCode, message)
this.onError = onError;
};
DecryptorV2.prototype.fetchCk_ = function
(ckName, contentKey, onError, nTriesLeft)
{
// The full name of the CK is
//
// <whatever-prefix>/CK/<ck-id> /ENCRYPTED-BY /<kek-prefix>/KEK/<key-id>
// \ / \ /
// ----------- ------------- ----------- -----------
// \/ \/
// from the encrypted data unknown (name in retrieved CK is used to determine KDK)
if (LOG > 3) console.log("Fetching CK " + ckName.toUri());
var thisDecryptor = this;
var onData = function(ckInterest, ckData) {
try {
contentKey.pendingInterest = 0;
// TODO: Verify that the key is legitimate.
var kdkPrefix = [null];
var kdkIdentityName = [null];
var kdkKeyName = [null];
if (!DecryptorV2.extractKdkInfoFromCkName_
(ckData.getName(), ckInterest.getName(), onError, kdkPrefix,
kdkIdentityName, kdkKeyName))
// The error has already been reported.
return;
// Check if the KDK already exists.
var kdkIdentity = null;
try {
// Debug: Use a Promise.
kdkIdentity = thisDecryptor.internalKeyChain_.getPib().getIdentity
(kdkIdentityName[0]);
} catch (ex) {
if (!(ex instanceof Pib.Error))
throw ex;
}
if (kdkIdentity != null) {
var kdkKey = null;
try {
// Debug: Use a Promise.
kdkKey = kdkIdentity.getKey(kdkKeyName[0]);
} catch (ex) {
if (!(ex instanceof Pib.Error))
throw ex;
}
if (kdkKey != null) {
// The KDK was already fetched and imported.
if (LOG > 3) console.log("KDK " + kdkKeyName.toUri() +
" already exists, so directly using it to decrypt the CK");
thisDecryptor.decryptCkAndProcessPendingDecrypts_
(contentKey, ckData, kdkKeyName[0], onError);
return;
}
}
thisDecryptor.fetchKdk_
(contentKey, kdkPrefix[0], ckData, onError, EncryptorV2.N_RETRIES);
} catch (ex) {
onError(EncryptError.ErrorCode.General, "Error in fetchCk onData: " + ex);
}
};
var onTimeout = function(interest) {
contentKey.pendingInterest = 0;
if (nTriesLeft > 1)
thisDecryptor.fetchCk_(ckName, contentKey, onError, nTriesLeft - 1);
else
onError(EncryptError.ErrorCode.CkRetrievalTimeout,
"Retrieval of CK [" + interest.getName().toUri() + "] timed out");
};
var onNetworkNack = function(interest, networkNack) {
contentKey.pendingInterest = 0;
onError(EncryptError.ErrorCode.CkRetrievalFailure,
"Retrieval of CK [" + interest.getName().toUri() +
"] failed. Got NACK (" + networkNack.getReason() + ")");
};
try {
contentKey.pendingInterest = this.face_.expressInterest
(new Interest(ckName).setMustBeFresh(false).setCanBePrefix(true),
onData, onTimeout, onNetworkNack);
} catch (ex) {
onError(EncryptError.ErrorCode.General, "expressInterest error: " + ex);
}
};
/**
*
* @param {DecryptorV2.ContentKey} contentKey
* @param {Name} kdkPrefix
* @param {Data} ckData
* @param {function} onError On error, this calls onError(errorCode, message).
* @param {number} nTriesLeft
*/
DecryptorV2.prototype.fetchKdk_ = function
(contentKey, kdkPrefix, ckData, onError, nTriesLeft)
{
// <kdk-prefix>/KDK/<kdk-id> /ENCRYPTED-BY /<credential-identity>/KEY/<key-id>
// \ / \ /
// ----------- ------------- --------------- ---------------
// \/ \/
// from the CK data from configuration
var kdkName = new Name(kdkPrefix);
kdkName
.append(EncryptorV2.NAME_COMPONENT_ENCRYPTED_BY)
.append(this.credentialsKey_.getName());
if (LOG > 3) console.log("Fetching KDK " + kdkName.toUri());
var thisDecryptor = this;
var onData = function(kdkInterest, kdkData) {
contentKey.pendingInterest = 0;
// TODO: Verify that the key is legitimate.
var isOk = thisDecryptor.decryptAndImportKdk_(kdkData, onError);
if (!isOk)
return;
// This way of getting the kdkKeyName is a bit hacky.
var kdkKeyName = kdkPrefix.getPrefix(-2)
.append("KEY").append(kdkPrefix.get(-1));
thisDecryptor.decryptCkAndProcessPendingDecrypts_
(contentKey, ckData, kdkKeyName, onError);
};
var onTimeout = function(interest) {
contentKey.pendingInterest = 0;
if (nTriesLeft > 1)
thisDecryptor.fetchKdk_
(contentKey, kdkPrefix, ckData, onError, nTriesLeft - 1);
else
onError(EncryptError.ErrorCode.KdkRetrievalTimeout,
"Retrieval of KDK [" + interest.getName().toUri() + "] timed out");
};
var onNetworkNack = function(interest, networkNack) {
contentKey.pendingInterest = 0;
onError(EncryptError.ErrorCode.KdkRetrievalFailure,
"Retrieval of KDK [" + interest.getName().toUri() +
"] failed. Got NACK (" + networkNack.getReason() + ")");
};
try {
contentKey.pendingInterest = this.face_.expressInterest
(new Interest(kdkName).setMustBeFresh(true).setCanBePrefix(false),
onData, onTimeout, onNetworkNack);
} catch (ex) {
onError(EncryptError.ErrorCode.General, "expressInterest error: " + ex);
}
};
/**
* @param {Data} kdkData
* @param {function} onError On error, this calls onError(errorCode, message).
* @returns {boolean} True for success, false for error (where this has called
* onError).
*/
DecryptorV2.prototype.decryptAndImportKdk_ = function(kdkData, onError)
{
try {
if (LOG > 3) console.log("Decrypting and importing KDK " +
kdkData.getName().toUri());
var encryptedContent = new EncryptedContent();
encryptedContent.wireDecodeV2(kdkData.getContent());
var safeBag = new SafeBag(encryptedContent.getPayload());
// Debug: Use a Promise.
var secret = SyncPromise.getValue(this.keyChain_.getTpm().decryptPromise
(encryptedContent.getPayloadKey().buf(), this.credentialsKey_.getName(), true));
if (secret.isNull()) {
onError(EncryptError.ErrorCode.TpmKeyNotFound,
"Could not decrypt secret, " + this.credentialsKey_.getName().toUri() +
" not found in TPM");
return false;
}
this.internalKeyChain_.importSafeBag(safeBag, secret.buf());
return true;
} catch (ex) {
// This can be EncodingException, Pib.Error, Tpm.Error, or a bunch of
// other runtime-derived errors.
onError(EncryptError.ErrorCode.DecryptionFailure,
"Failed to decrypt KDK [" + kdkData.getName().toUri() + "]: " + ex);
return false;
}
};
/**
* @param {DecryptorV2.ContentKey} contentKey
* @param {Data} ckData
* @param {Name} kdkKeyName
* @param {function} onError On error, this calls onError(errorCode, message).
*/
DecryptorV2.prototype.decryptCkAndProcessPendingDecrypts_ = function
(contentKey, ckData, kdkKeyName, onError)
{
if (LOG > 3) console.log("Decrypting CK data " + ckData.getName().toUri());
var content = new EncryptedContent();
try {
content.wireDecodeV2(ckData.getContent());
} catch (ex) {
onError(EncryptError.ErrorCode.InvalidEncryptedFormat,
"Error decrypting EncryptedContent: " + ex);
return;
}
var ckBits;
try {
// Debug: Use a Promise.
ckBits = SyncPromise.getValue(this.internalKeyChain_.getTpm().decryptPromise
(content.getPayload().buf(), kdkKeyName, true));
} catch (ex) {
// We don't expect this from the in-memory KeyChain.
onError(EncryptError.ErrorCode.DecryptionFailure,
"Error decrypting the CK EncryptedContent " + ex);
return;
}
if (ckBits.isNull()) {
onError(EncryptError.ErrorCode.TpmKeyNotFound,
"Could not decrypt secret, " + kdkKeyName.toUri() + " not found in TPM");
return;
}
contentKey.bits = ckBits;
contentKey.isRetrieved = true;
for (var i in contentKey.pendingDecrypts) {
var pendingDecrypt = contentKey.pendingDecrypts[i];
// TODO: If this calls onError, should we quit?
DecryptorV2.doDecrypt_
(pendingDecrypt.encryptedContent, contentKey.bits,
pendingDecrypt.onSuccess, pendingDecrypt.onError);
}
contentKey.pendingDecrypts = [];
};
/**
* @param {EncryptedContent} content
* @param {Blob} ckBits
* @param {function} onSuccess On success, this calls onSuccess(plainData)
* where plainData is a Blob.
* @param {function} onError On error, this calls onError(errorCode, message).
*/
DecryptorV2.doDecrypt_ = function(content, ckBits, onSuccess, onError)
{
if (!content.hasInitialVector()) {
onError(EncryptError.ErrorCode.MissingRequiredInitialVector,
"Expecting Initial Vector in the encrypted content, but it is not present");
return;
}
var plainData;
try {
var params = new EncryptParams(EncryptAlgorithmType.AesCbc);
params.setInitialVector(content.getInitialVector());
// Debug: Use a Promise.
plainData = AesAlgorithm.decrypt(ckBits, content.getPayload(), params);
} catch (ex) {
onError(EncryptError.ErrorCode.DecryptionFailure,
"Decryption error in doDecrypt: " + ex);
return;
}
try {
onSuccess(plainData);
} catch (ex) {
console.log("Error in onSuccess: " + NdnCommon.getErrorWithStackTrace(ex));
}
};
/**
* Convert the KEK name to the KDK prefix:
* <access-namespace>/KEK/<key-id> ==> <access-namespace>/KDK/<key-id>.
* @param {Name} kekName The KEK name.
* @param {function} onError This calls onError.onError(errorCode, message) for
* an error.
* @return {Name} The KDK prefix, or null if an error was reported to onError.
*/
DecryptorV2.convertKekNameToKdkPrefix_ = function(kekName, onError)
{
if (kekName.size() < 2 ||
!kekName.get(-2).equals(EncryptorV2.NAME_COMPONENT_KEK)) {
onError(EncryptError.ErrorCode.KekInvalidName,
"Invalid KEK name [" + kekName.toUri() + "]");
return null;
}
return kekName.getPrefix(-2)
.append(EncryptorV2.NAME_COMPONENT_KDK).append(kekName.get(-1));
};
/**
* Extract the KDK information from the CK Data packet name. The KDK identity
* name plus the KDK key ID together identify the KDK private key in the KeyChain.
* @param {Name} ckDataName The name of the CK Data packet.
* @param {Name} ckName The CK name from the Interest used to fetch the CK Data
* packet.
* @param {function} onError This calls onError.onError(errorCode, message) for
* an error.
* @param {Array<Name>} kdkPrefix This sets kdkPrefix[0] to the KDK prefix.
* @param {Array<Name>} kdkIdentityName This sets kdkIdentityName[0] to the KDK
* identity name.
* @param {Array<Name>} kdkKeyId This sets kdkKeyId[0] to the KDK key ID.
* @return {boolean} True for success or false if an error was reported to
* onError.
*/
DecryptorV2.extractKdkInfoFromCkName_ = function
(ckDataName, ckName, onError, kdkPrefix, kdkIdentityName, kdkKeyId)
{
// <full-ck-name-with-id> | /ENCRYPTED-BY/<kek-prefix>/NAC/KEK/<key-id>
if (ckDataName.size() < ckName.size() + 1 ||
!ckDataName.getPrefix(ckName.size()).equals(ckName) ||
!ckDataName.get(ckName.size()).equals(EncryptorV2.NAME_COMPONENT_ENCRYPTED_BY)) {
onError(EncryptError.ErrorCode.CkInvalidName,
"Invalid CK name [" + ckDataName.toUri() + "]");
return false;
}
var kekName = ckDataName.getSubName(ckName.size() + 1);
kdkPrefix[0] = DecryptorV2.convertKekNameToKdkPrefix_(kekName, onError);
if (kdkPrefix[0] == null)
// The error has already been reported.
return false;
kdkIdentityName[0] = kekName.getPrefix(-2);
kdkKeyId[0] = kekName.getPrefix(-2).append("KEY").append(kekName.get(-1));
return true;
};