/**
* 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.
*/
// Don't require modules since this is meant for the browser, not Node.js.
/**
* IndexedDbIdentityStorage extends IdentityStorage and implements its methods
* to store identity, public key and certificate objects using the browser's
* IndexedDB service.
* @constructor
*/
var IndexedDbIdentityStorage = function IndexedDbIdentityStorage()
{
IdentityStorage.call(this);
this.database = new Dexie("ndnsec-public-info");
// The database schema imitates MemoryIdentityStorage.
this.database.version(1).stores({
// A table for global values. It currently only has the defaultIdentityUri.
// "key" is the key like "defaultIdentityUri" // string
// "value" is the value. For "defaultIdentityUri" the value is the
// default identity name URI, or absent if not defined. // string
globals: "key",
// "identityNameUri" is the identity name URI // string
// "defaultKeyUri" is the default key name URI or null // string
identity: "identityNameUri",
// "keyNameUri" is the key name URI // string
// "keyType" is the type of the public key // number from KeyType
// "keyDer" is the public key DER // Uint8Array
// "defaultCertificateUri" is the default cert name URI or null // string
publicKey: "keyNameUri",
// "certificateNameUri" is the certificate name URI // string
// "encoding" is the certificate wire encoding // Uint8Array
certificate: "certificateNameUri"
});
this.database.open();
};
IndexedDbIdentityStorage.prototype = new IdentityStorage();
IndexedDbIdentityStorage.prototype.name = "IndexedDbIdentityStorage";
/**
* Check if the specified identity already exists.
* @param {Name} identityName The identity name.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @returns {Promise} A promise which returns true if the identity exists.
*/
IndexedDbIdentityStorage.prototype.doesIdentityExistPromise = function
(identityName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.doesIdentityExistPromise is only supported for async")));
return this.database.identity.where("identityNameUri").equals
(identityName.toUri())
.count()
.then(function(count) {
return Promise.resolve(count > 0);
});
};
/**
* Add a new identity. Do nothing if the identity already exists.
* @param {Name} identityName The identity name to be added.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the identity is added.
*/
IndexedDbIdentityStorage.prototype.addIdentityPromise = function
(identityName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.addIdentityPromise is only supported for async")));
var thisStorage = this;
return this.doesIdentityExistPromise(identityName)
.then(function(exists) {
if (exists)
// Do nothing.
return Promise.resolve();
return thisStorage.database.identity.put
({ identityNameUri: identityName.toUri(), defaultKeyUri: null });
});
};
/**
* Check if the specified key already exists.
* @param {Name} keyName The name of the key.
* @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.
*/
IndexedDbIdentityStorage.prototype.doesKeyExistPromise = function
(keyName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.doesKeyExistPromise is only supported for async")));
return this.database.publicKey.where("keyNameUri").equals(keyName.toUri())
.count()
.then(function(count) {
return Promise.resolve(count > 0);
});
};
/**
* Add a public key to the identity storage. Also call addIdentity to ensure
* that the identityName for the key exists. However, if the key already
* exists, do nothing.
* @param {Name} keyName The name of the public key to be added.
* @param {number} keyType Type of the public key to be added from KeyType, such
* as KeyType.RSA..
* @param {Blob} publicKeyDer A blob of the public key DER to be added.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when complete.
*/
IndexedDbIdentityStorage.prototype.addKeyPromise = function
(keyName, keyType, publicKeyDer, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.addKeyPromise is only supported for async")));
if (keyName.size() === 0)
return Promise.resolve();
var thisStorage = this;
return this.doesKeyExistPromise(keyName)
.then(function(exists) {
if (exists)
return Promise.resolve();
var identityName = keyName.getPrefix(-1);
return thisStorage.addIdentityPromise(identityName)
.then(function() {
return thisStorage.database.publicKey.put
({ keyNameUri: keyName.toUri(), keyType: keyType,
keyDer: new Blob(publicKeyDer, true).buf(),
defaultCertificate: null });
});
});
};
/**
* Get the public key DER blob from the identity storage.
* @param {Name} keyName The name of the requested public key.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which returns the DER Blob, or a promise rejected
* with SecurityException if the key doesn't exist.
*/
IndexedDbIdentityStorage.prototype.getKeyPromise = function(keyName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.getKeyPromise is only supported for async")));
if (keyName.size() === 0)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage::getKeyPromise: Empty keyName")));
return this.database.publicKey.get(keyName.toUri())
.then(function(publicKeyEntry) {
if (publicKeyEntry)
return Promise.resolve(new Blob(publicKeyEntry.keyDer));
else
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage::getKeyPromise: The key does not exist")));
});
};
/**
* Check if the specified certificate already exists.
* @param {Name} certificateName The name of the certificate.
* @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 certificate exists.
*/
IndexedDbIdentityStorage.prototype.doesCertificateExistPromise = function
(certificateName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.doesCertificateExistPromise is only supported for async")));
return this.database.certificate.where("certificateNameUri").equals
(certificateName.toUri())
.count()
.then(function(count) {
return Promise.resolve(count > 0);
});
};
/**
* Add a certificate to the identity storage. Also call addKey to ensure that
* the certificate key exists. If the certificate is already installed, don't
* replace it.
* @param {IdentityCertificate} certificate The certificate to be added. This
* makes a copy of the certificate.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when finished.
*/
IndexedDbIdentityStorage.prototype.addCertificatePromise = function
(certificate, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.addCertificatePromise is only supported for async")));
var certificateName = certificate.getName();
var keyName = certificate.getPublicKeyName();
var thisStorage = this;
return this.addKeyPromise
(keyName, certificate.getPublicKeyInfo().getKeyType(),
certificate.getPublicKeyInfo().getKeyDer(), useSync)
.then(function() {
return thisStorage.doesCertificateExistPromise(certificateName);
})
.then(function(exists) {
if (exists)
return Promise.resolve();
// Insert the certificate.
// wireEncode returns the cached encoding if available.
return thisStorage.database.certificate.put
({ certificateNameUri: certificateName.toUri(),
encoding: certificate.wireEncode().buf() });
});
};
/**
* Get a certificate from the identity storage.
* @param {Name} certificateName The name of the requested certificate.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which returns the requested
* IdentityCertificate, or a promise rejected with SecurityException if the
* certificate doesn't exist.
*/
IndexedDbIdentityStorage.prototype.getCertificatePromise = function
(certificateName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.getCertificatePromise is only supported for async")));
return this.database.certificate.get(certificateName.toUri())
.then(function(certificateEntry) {
if (certificateEntry) {
var certificate = new IdentityCertificate();
try {
certificate.wireDecode(certificateEntry.encoding);
} catch (ex) {
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage::getCertificatePromise: The certificate cannot be decoded")));
}
return Promise.resolve(certificate);
}
else
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage::getCertificatePromise: The certificate does not exist")));
});
};
/*****************************************
* Get/Set Default *
*****************************************/
/**
* Get the default identity.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which returns the Name of default identity,
* or a promise rejected with SecurityException if the default identity is not
* set.
*/
IndexedDbIdentityStorage.prototype.getDefaultIdentityPromise = function(useSync)
{
return this.database.globals.get("defaultIdentityUri")
.then(function(defaultIdentityEntry) {
if (defaultIdentityEntry)
return Promise.resolve(new Name(defaultIdentityEntry.value));
else
throw new SecurityException(new Error
("IndexedDbIdentityStorage.getDefaultIdentity: The default identity is not defined"));
});
};
/**
* Get the default key name for the specified identity.
* @param {Name} identityName The identity name.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which returns the default key Name, or a
* promise rejected with SecurityException if the default key name for the
* identity is not set.
*/
IndexedDbIdentityStorage.prototype.getDefaultKeyNameForIdentityPromise = function
(identityName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.getDefaultKeyNameForIdentityPromise is only supported for async")));
return this.database.identity.get(identityName.toUri())
.then(function(identityEntry) {
if (identityEntry) {
if (identityEntry.defaultKeyUri != null)
return Promise.resolve(new Name(identityEntry.defaultKeyUri));
else
throw new SecurityException(new Error("No default key set."));
}
else
throw new SecurityException(new Error("Identity not found."));
});
};
/**
* Get the default certificate name for the specified key.
* @param {Name} keyName The key name.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which returns the default certificate Name,
* or a promise rejected with SecurityException if the default certificate name
* for the key name is not set.
*/
IndexedDbIdentityStorage.prototype.getDefaultCertificateNameForKeyPromise = function
(keyName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.getDefaultCertificateNameForKeyPromise is only supported for async")));
return this.database.publicKey.get(keyName.toUri())
.then(function(publicKeyEntry) {
if (publicKeyEntry) {
if (publicKeyEntry.defaultCertificateUri != null)
return Promise.resolve(new Name(publicKeyEntry.defaultCertificateUri));
else
throw new SecurityException(new Error("No default certificate set."));
}
else
throw new SecurityException(new Error("Key not found."));
});
};
/**
* Append all the identity names to the nameList.
* @param {Array<Name>} nameList Append result names to nameList.
* @param {boolean} isDefault If true, add only the default identity name. If
* false, add only the non-default identity names.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the names are added to
* nameList.
*/
IndexedDbIdentityStorage.prototype.getAllIdentitiesPromise = function
(nameList, isDefault, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.getAllIdentitiesPromise is only supported for async")));
var defaultIdentityName = null;
var thisStorage = this;
return this.getDefaultIdentityPromise()
.then(function(localDefaultIdentityName) {
defaultIdentityName = localDefaultIdentityName;
return SyncPromise.resolve();
}, function(err) {
// The default identity name was not found.
return SyncPromise.resolve();
})
.then(function() {
return thisStorage.database.identity.each(function(identityEntry) {
var identityName = new Name(identityEntry.identityNameUri);
var identityNameIsDefault =
(defaultIdentityName !== null && identityName.equals(defaultIdentityName));
if (isDefault && identityNameIsDefault)
nameList.push(identityName);
else if (!isDefault && !identityNameIsDefault)
nameList.push(identityName);
});
});
};
/**
* Append all the key names of a particular identity to the nameList.
* @param {Name} identityName The identity name to search for.
* @param {Array<Name>} nameList Append result names to nameList.
* @param {boolean} isDefault If true, add only the default key name. If false,
* add only the non-default key names.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the names are added to
* nameList.
*/
IndexedDbIdentityStorage.prototype.getAllKeyNamesOfIdentityPromise = function
(identityName, nameList, isDefault, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.getAllKeyNamesOfIdentityPromise is only supported for async")));
var defaultKeyName = null;
var thisStorage = this;
return this.getDefaultKeyNameForIdentityPromise(identityName)
.then(function(localDefaultKeyName) {
defaultKeyName = localDefaultKeyName;
return SyncPromise.resolve();
}, function(err) {
// The default key name was not found.
return SyncPromise.resolve();
})
.then(function() {
// Iterate through each publicKey a to find ones that match identityName.
// This is a little inefficient, but we don't expect the in-browser
// database to be very big, we don't expect to use this function often (for
// deleting an identity), and this is simpler than complicating the database
// schema to store the identityName with each publicKey.
return thisStorage.database.publicKey.each(function(publicKeyEntry) {
var keyName = new Name(publicKeyEntry.keyNameUri);
var keyIdentityName = keyName.getPrefix(-1);
if (keyIdentityName.equals(identityName)) {
var keyNameIsDefault =
(defaultKeyName !== null && keyName.equals(defaultKeyName));
if (isDefault && keyNameIsDefault)
nameList.push(keyName);
else if (!isDefault && !keyNameIsDefault)
nameList.push(keyName);
}
});
});
};
/**
* Append all the certificate names of a particular key name to the nameList.
* @param {Name} keyName The key name to search for.
* @param {Array<Name>} nameList Append result names to nameList.
* @param {boolean} isDefault If true, add only the default certificate name.
* If false, add only the non-default certificate names.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the names are added to
* nameList.
*/
IndexedDbIdentityStorage.prototype.getAllCertificateNamesOfKeyPromise = function
(keyName, nameList, isDefault, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.getAllCertificateNamesOfKeyPromise is only supported for async")));
var defaultCertificateName = null;
var thisStorage = this;
return this.getDefaultCertificateNameForKeyPromise(keyName)
.then(function(localDefaultCertificateName) {
defaultCertificateName = localDefaultCertificateName;
return SyncPromise.resolve();
}, function(err) {
// The default certificate name was not found.
return SyncPromise.resolve();
})
.then(function() {
// Iterate through each certificate record a to find ones that match keyName.
// This is a little inefficient, but we don't expect the in-browser
// database to be very big, we don't expect to use this function often (for
// deleting an identity), and this is simpler than complicating the database
// schema to store the keyName with each certificate record.
return thisStorage.database.certificate.each(function(certificateEntry) {
var certificateName = new Name(certificateEntry.certificateNameUri);
var certificateKeyName = IdentityCertificate.certificateNameToPublicKeyName
(certificateName);
if (certificateKeyName.equals(keyName)) {
var certificateNameIsDefault =
(defaultCertificateName !== null &&
certificateName.equals(defaultCertificateName));
if (isDefault && certificateNameIsDefault)
nameList.push(certificateName);
else if (!isDefault && !certificateNameIsDefault)
nameList.push(certificateName);
}
});
});
};
/**
* Set the default identity. If the identityName does not exist, then clear the
* default identity so that getDefaultIdentity() throws an exception.
* @param {Name} identityName The default identity name.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the default identity is set.
*/
IndexedDbIdentityStorage.prototype.setDefaultIdentityPromise = function
(identityName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.setDefaultIdentityPromise is only supported for async")));
var thisStorage = this;
return this.doesIdentityExistPromise(identityName)
.then(function(exists) {
if (exists)
return thisStorage.database.globals.put
({ key: "defaultIdentityUri", value: identityName.toUri() });
else
// The identity doesn't exist, so clear the default.
return thisStorage.database.globals.delete("defaultIdentityUri");
});
};
/**
* Set a key as the default key of an identity. The identity name is inferred
* from keyName.
* @param {Name} keyName The name of the key.
* @param {Name} identityNameCheck (optional) The identity name to check that the
* keyName contains the same identity name. If an empty name, it is ignored.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the default key name is
* set.
*/
IndexedDbIdentityStorage.prototype.setDefaultKeyNameForIdentityPromise = function
(keyName, identityNameCheck, useSync)
{
useSync = (typeof identityNameCheck === "boolean") ? identityNameCheck : useSync;
identityNameCheck = (identityNameCheck instanceof Name) ? identityNameCheck : null;
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.setDefaultKeyNameForIdentityPromise is only supported for async")));
var identityName = keyName.getPrefix(-1);
if (identityNameCheck != null && identityNameCheck.size() > 0 &&
!identityNameCheck.equals(identityName))
return Promise.reject(new SecurityException(new Error
("The specified identity name does not match the key name")));
// update does nothing if the identityName doesn't exist.
return this.database.identity.update
(identityName.toUri(), { defaultKeyUri: keyName.toUri() });
};
/**
* Set the default key name for the specified identity.
* @param {Name} keyName The key name.
* @param {Name} certificateName The certificate name.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the default certificate
* name is set.
*/
IndexedDbIdentityStorage.prototype.setDefaultCertificateNameForKeyPromise = function
(keyName, certificateName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.setDefaultCertificateNameForKeyPromise is only supported for async")));
// update does nothing if the keyName doesn't exist.
return this.database.publicKey.update
(keyName.toUri(), { defaultCertificateUri: certificateName.toUri() });
};
/*****************************************
* Delete Methods *
*****************************************/
/**
* Delete a certificate.
* @param {Name} certificateName The certificate name.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the certificate info is
* deleted.
*/
IndexedDbIdentityStorage.prototype.deleteCertificateInfoPromise = function
(certificateName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.deleteCertificateInfoPromise is only supported for async")));
if (certificateName.size() === 0)
return Promise.resolve();
return this.database.certificate.delete(certificateName.toUri());
};
/**
* Delete a public key and related certificates.
* @param {Name} keyName The key name.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the public key info is
* deleted.
*/
IndexedDbIdentityStorage.prototype.deletePublicKeyInfoPromise = function
(keyName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.deletePublicKeyInfoPromise is only supported for async")));
if (keyName.size() === 0)
return Promise.resolve();
var thisStorage = this;
return this.database.publicKey.delete(keyName.toUri())
.then(function() {
// Iterate through each certificate to find ones that match keyName. This is
// a little inefficient, but we don't expect the in-browswer database to be
// very big, we don't expect to delete often, and this is simpler than
// complicating the database schema to store the keyName with each certificate.
return thisStorage.database.certificate.each(function(certificateEntry) {
if (IdentityCertificate.certificateNameToPublicKeyName
(new Name(certificateEntry.certificateNameUri)).equals(keyName))
thisStorage.database.certificate.delete
(certificateEntry.certificateNameUri);
});
});
};
/**
* Delete an identity and related public keys and certificates.
* @param {Name} identityName The identity name.
* @param {boolean} useSync (optional) If true then return a rejected promise
* since this only supports async code.
* @return {Promise} A promise which fulfills when the identity info is
* deleted.
*/
IndexedDbIdentityStorage.prototype.deleteIdentityInfoPromise = function
(identityName, useSync)
{
if (useSync)
return Promise.reject(new SecurityException(new Error
("IndexedDbIdentityStorage.deleteIdentityInfoPromise is only supported for async")));
var thisStorage = this;
return this.database.identity.delete(identityName.toUri())
// Iterate through each publicKey and certificate to find ones that match
// identityName. This is a little inefficient, but we don't expect the
// in-browswer database to be very big, we don't expect to delete often, and
// this is simpler than complicating the database schema to store the
// identityName with each publicKey and certificate.
.then(function() {
return thisStorage.database.publicKey.each(function(publicKeyEntry) {
var keyIdentityName = new Name(publicKeyEntry.keyNameUri).getPrefix(-1);
if (keyIdentityName.equals(identityName))
thisStorage.database.publicKey.delete(publicKeyEntry.keyNameUri);
});
})
.then(function() {
return thisStorage.database.certificate.each(function(certificateEntry) {
var certificateKeyName = IdentityCertificate.certificateNameToPublicKeyName
(new Name(certificateEntry.certificateNameUri));
var certificateIdentityName = certificateKeyName.getPrefix(-1);
if (certificateIdentityName.equals(identityName))
thisStorage.database.certificate.delete
(certificateEntry.certificateNameUri);
});
});
};