oidc-lib
Version:
A library for creating OIDC Service Providers
595 lines (532 loc) • 26.8 kB
JavaScript
const METADATACOLLECTIONNAME = "_exchangeMetadata";
const _CONSTRUCTOR = 'indexed_db_provider';
module.exports = {
_constructor: _CONSTRUCTOR,
indexed_db_provider: indexed_db_provider,
METADATACOLLECTIONNAME: METADATACOLLECTIONNAME,
};
var pk = null;
var c_key = null;
function indexed_db_provider(){
Object.defineProperty(this, "initialize", {
value: function(pkInput, contentModuleName, provider){
return new Promise((resolve, reject) => {
pk = pkInput;
var dbInfo = {
provider: provider,
contentModuleName: contentModuleName
};
validateModuleConfig(contentModuleName);
dbInfo.schemaVersion = getDbModuleConfigProperty(contentModuleName, 'schemaVersion');
dbInfo.databaseName = getDbModuleConfigProperty(contentModuleName, 'databaseName');
dbInfo.databaseDefinition = { id: dbInfo.databaseName };
dbInfo.databaseUrl = `dbs/${dbInfo.databaseDefinition.id}`;
// dbInfo.c_key is set in wallet_op.c
dbInfo.collections = {};
var collections = getDbModuleConfigProperty(contentModuleName, 'collections');
for (var collectionName in collections){
var collection = collections[collectionName];
if (!collection.publicProperties){
collection.publicProperties = [];
}
var collectionInfo = {
keyPath: collection.keyPath,
indexes: collection.indexes,
publicProperties: collection.publicProperties,
highWater: 0,
encryption: collection.encryption,
hubContext: collection.hubContext ? collection.hubContext : 'https://schema.org',
hubInterface: collection.hubInterface ? collection.hubInterface : 'Collections',
hubType: collection.hubType ? collection.hubType : collectionName
};
collectionInfo.publicProperties.push("didMeta");
collectionInfo.publicProperties.push(collectionInfo.keyPath);
for (var i=0; i < collectionInfo.indexes.length; i++){
var index = collectionInfo.indexes[i];
if (Array.isArray(index.keyPath)){
for (var j=0; j < index.keyPath.length; j++){
var field = index.keyPath[j];
if (collectionInfo.publicProperties.indexOf(field) < 0){
collectionInfo.publicProperties.push(field);
}
}
}
else{
if (collectionInfo.publicProperties.indexOf(index.keyPath) < 0){
collectionInfo.publicProperties.push(index.keyPath);
}
}
}
dbInfo.collections[collectionName] = collectionInfo;
}
getDatabase(dbInfo)
.then(db => {
if (db){
// in other databases we need to cycle through get_collections
// here the collections are created by schema applied
// in getDatabase by a change to the database version number.
dbInfo.db = db;
resolve(dbInfo);
}
else{
reject('Database initialization failed for module ' + contentModuleName);
}
}, err => {
reject(err);
})
})
}
})
Object.defineProperty(this, "initialize_local_storage", {
value: function(pkInput, contentModuleName, provider){
return new Promise((resolve, reject) => {
pk = pkInput;
var localStorage = pk.util.config.localStorage;
var dbInfo = {
provider: provider,
contentModuleName: contentModuleName
};
dbInfo.schemaVersion = localStorage['schemaVersion'];
dbInfo.databaseName = localStorage['databaseName'];
dbInfo.databaseDefinition = { id: dbInfo.databaseName };
dbInfo.databaseUrl = `dbs/${dbInfo.databaseDefinition.id}`;
dbInfo.collections = {};
var collections = localStorage['collections'];
for (var collectionName in collections){
var collection = collections[collectionName];
var collectionInfo = {
keyPath: collection.keyPath,
indexes: collection.indexes,
publicProperties: [],
highWater: 0,
encryption: collection.encryption,
hubContext: collection.hubContext ? collection.hubContext : 'https://schema.org',
hubInterface: collection.hubInterface ? collection.hubInterface : 'Collections',
hubType: collection.hubType ? collection.hubType : collectionName
};
collectionInfo.publicProperties.push(collectionInfo.keyPath);
for (var i=0; i < collectionInfo.indexes.length; i++){
var index = collectionInfo.indexes[i];
if (Array.isArray(index.keyPath)){
for (var j=0; j < index.keyPath.length; j++){
var field = index.keyPath[j];
if (collectionInfo.publicProperties.indexOf(field) < 0){
collectionInfo.publicProperties.push(field);
}
}
}
else{
if (collectionInfo.publicProperties.indexOf(index.keyPath) < 0){
collectionInfo.publicProperties.push(index.keyPath);
}
}
}
dbInfo.collections[collectionName] = collectionInfo;
}
getDatabase(dbInfo)
.then(db => {
if (db){
// in other databases we need to cycle through get_collections
// here the collections are created by schema applied
// in getDatabase by a change to the database version number.
dbInfo.db = db;
resolve(dbInfo);
}
}, err => {
reject(err);
})
.then(function() {}, function(err){
if (err.code !== undefined){
switch (err.code){
case 'ENOENT':
if (err.syscall === 'getaddrinfo'){
pk.util.log_debug('Unable to lookup database service in DNS in module ' + contentModuleName);
pk.util.log_debug('Hostname: ' + err.hostname);
return;
}
break;
default:
break;
}
}
reject('Database initialization failed for module ' + contentModuleName);
})
})
}
})
function getDatabase (dbInfo) {
return new Promise((resolve, reject) => {
var version = dbInfo.schemaVersion;
var request = indexedDB.open(dbInfo.databaseName, version);
request.onerror = function(event) {
pk.util.log_detail('error opening database', event);
reject('error opening database' + event.currentTarget.error.message);
}
request.onsuccess = function(event) {
resolve(event.target.result);
};
request.onupgradeneeded = function(event) {
var db = event.target.result;
for (var collectionName in dbInfo.collections){
if (db.objectStoreNames.contains(collectionName)){
var objectStore = db.deleteObjectStore(collectionName);
}
var collectionInfo = dbInfo.collections[collectionName];
var objectStore = db.createObjectStore(collectionName, { keyPath: collectionInfo.keyPath });
if (collectionInfo.indexes !== undefined){
for (var i=0; i < collectionInfo.indexes.length; i++){
var indexInfo = collectionInfo.indexes[i];
var params = {};
if (indexInfo.unique){
params.unique = true;
}
var result = objectStore.createIndex(indexInfo.name, indexInfo.keyPath, params);
}
}
}
}
})
}
Object.defineProperty(this, "createOrUpdateDocument", {
value: function(dbInfo, collectionName, objectToWrite, options) {
return new Promise((resolve, reject) => {
try{
if (!objectToWrite.didMeta){
objectToWrite.didMeta = {};
}
if (!objectToWrite.didMeta.object_id){
objectToWrite.didMeta.object_id = objectToWrite.id;
}
if (!options || !options.suppress_time_update){
var date = new Date();
var update_time = date.getTime();
objectToWrite.didMeta.update_time = update_time;
}
if (dbInfo.collections[collectionName] === undefined){
reject('cannot createOrUpdate on unknown collection: ' + collectionName);
}
var encryptedObject = db_encrypt(dbInfo, collectionName, objectToWrite);
var transaction = dbInfo.db.transaction([collectionName], "readwrite");
// report on the success of opening the transaction
transaction.onerror = function(event) {
pk.util.log_debug('<li>Transaction not opened: ', event);
};
var objectStore = transaction.objectStore(collectionName);
var request = objectStore.put(encryptedObject);
request.onsuccess = function(event) {
resolve(objectToWrite);
};
request.onerror = function(event) {
reject(event.target.error + ": " + collectionName);
}
}
catch(err){
pk.util.log_error('createOrUpdataDocument', err);
reject(err);
}
})
}
})
Object.defineProperty(this, "getDocument", {
value: function (dbInfo, collectionName, param3, param4) {
var index_id;
if (param4){
index_id = param3;
local_id = param4;
}
else{
local_id = param3;
}
var collectionInfo = dbInfo.collections[collectionName];
return new Promise((resolve, reject) => {
var transaction = dbInfo.db.transaction([collectionName]);
var objectStore = transaction.objectStore(collectionName);
var request;
if (index_id){
var keyRangeValue = IDBKeyRange.lowerBound(local_id);
var index = objectStore.index(index_id);
index.openCursor(keyRangeValue).onsuccess = function(event) {
var cursor = event.target.result;
if(cursor) {
if (cursor.value.did === local_id){
var record = cursor.value;
db_decrypt(dbInfo, collectionName, record)
.then(result => {
resolve(result);
}, err => {
console.log("error1 in getDocument");
reject('error1 in getDocument');
})
}
else{
cursor.continue();
}
} else {
reject('did not located');
}
};
index.onerror = function(event) {
reject('error getting document');
};
}
else{
request = objectStore.get(local_id);
request.onerror = function(event) {
alert("error getting document");
reject(key.target.error);
};
request.onsuccess = function(event) {
if (event.target.result === undefined){
var err = {
msg: "Document could not be found",
code: 404
};
reject(err);
}
else{
var record = event.target.result;
db_decrypt(dbInfo, collectionName, record)
.then (result => {
resolve(result);
}, err => {
console.log('error2 in getDocument');
reject('error2 in getDocument');
})
}
}
}
})
}
})
Object.defineProperty(this, "deleteDocument", {
value: function(dbInfo, collectionName, local_id){
return new Promise((resolve, reject) => {
var transaction = dbInfo.db.transaction([collectionName], "readwrite");
transaction.onsuccess = function(event){
resolve(true);
};
transaction.onerror = function(event){
reject("transaction failed");
}
var objectStore = transaction.objectStore(collectionName);
var request = objectStore.delete(local_id);
request.onsuccess = function(event) {
resolve(true);
};
request.onerror = function(event) {
reject('document did not exist');
}
})
}
})
Object.defineProperty(this, "queryCollection", {
value: function(dbInfo, collectionName, queryDictionaryOrString) {
var queryResult = [];
return new Promise((resolve, reject) => {
var qDebug = false;
if (qDebug){
console.log('\r\n\r\n*****************************************************************************');
console.log('qDebug: collectionName: ' + collectionName);
console.log('qDebug: queryDictionaryOrString', JSON.stringify(queryDictionaryOrString, null, 4) + '\r\n');
console.log('*****************************************************************************\r\n');
}
if (typeof queryDictionaryOrString !== 'object'){
throw ('indexed_db queryCollection currently does not support string queries');
}
var collectionInfo = dbInfo.collections[collectionName];
var objectStore = dbInfo.db.transaction(collectionName).objectStore(collectionName);
var cursor = objectStore.openCursor();
cursor.onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
var match = event.target.result.value;
if (qDebug){
console.log('qDebug: cursor value ', JSON.stringify(match, null, 4) + '\r\n');
}
var allClaimsOK = true
for (var claimName in queryDictionaryOrString){
var dictValue = queryDictionaryOrString[claimName];
if (dictValue !== match[claimName]){
if (qDebug){
console.log('qDebug: no match on ' + claimName);
}
allClaimsOK = false;
break;
}
}
if (qDebug){
console.log('qDebug: allClaimsOK ', allClaimsOK)
}
if (allClaimsOK){
queryResult.push(match);
}
cursor.continue();
}
else {
// iteration complete
for (var i=0; i < queryResult.length; i++){
db_decrypt(dbInfo, collectionName, queryResult[i])
.then(result => {
if (qDebug){
console.log('qDebug: push result ', JSON.stringify(result, null, 4) + '\r\n');
}
}, err => {
console.log('error3 in queryCollection: ', err);
})
}
resolve(queryResult);
}
};
cursor.onerror = function(event){
alert('cursor onError');
}
})
}
})
function validateModuleConfig(contentModuleName){
if (contentModuleName !== 'sts'){
if (pk.util.config.content_modules[contentModuleName].db === undefined){
throw "The " + contentModuleName + " content module does not define a db property in claimer_config";
}
}
}
function getDbModuleConfigProperty(contentModuleName, propertyName, throwOnUndefined)
{
if (throwOnUndefined === undefined || throwOnUndefined === true){
var dbInstance = pk.util.config.sts.db;
if (contentModuleName !== 'sts'){
dbInstance = pk.util.config.content_modules[contentModuleName].db;
}
if (dbInstance[propertyName] === undefined){
throw "Configuration property " + propertyName + " is not defined in content module " + contentModuleName;
}
}
return dbInstance[propertyName];
}
function db_encrypt(dbInfo, collectionName, objectToWrite){
if (dbInfo.c_key === false){
return objectToWrite;
}
var toEncrypt = {};
var collection = dbInfo.collections[collectionName];
var publicProperties = {};
var doEncrypt = false;
for (var propertyName in objectToWrite){
if (collection.publicProperties.includes(propertyName)){
publicProperties[propertyName] = objectToWrite[propertyName];
continue;
}
var newValue = undefined;
switch (propertyName){
case 'private':
case 'public':
case 'symmetric':
var propertyValue = objectToWrite[propertyName];
if (typeof propertyValue === 'object' &&
propertyValue.constructor.name === 'CryptoKey'){
newValue = {};
newValue.isCryptoKey = true;
newValue.algorithm = propertyValue.algorithm;
newValue.extractable = propertyValue.extractable;
newValue.type = propertyValue.type;
newValue.usages = propertyValue.usages;
}
break;
default:
break;
}
toEncrypt[propertyName] = newValue ? newValue : objectToWrite[propertyName];
doEncrypt = true;
}
if (!doEncrypt){
return objectToWrite;
}
var bufferToEncrypt = forge.util.createBuffer(JSON.stringify(toEncrypt), 'utf8');
var encryptedProperties = pk.pmanager.wallet_encrypt("AES-CBC", dbInfo.c_key, bufferToEncrypt);
publicProperties['didMeta']['encrypted'] = encryptedProperties;
return publicProperties;
}
async function db_decrypt(dbInfo, collectionName, objectToReturn){
try {
var potentialError = 'db_decrypt';
var propertyName;
var keyData;
var decryptedProperties;
if (dbInfo.c_key === false){
return objectToReturn;
}
// nothing to decrypt
if (!objectToReturn.didMeta.encrypted){
return objectToReturn;
}
var collection = dbInfo.collections[collectionName];
if (objectToReturn.didMeta.encrypted){
var decryptedPayload = pk.pmanager.wallet_decrypt("AES-CBC", dbInfo.c_key, objectToReturn.didMeta.encrypted);
delete objectToReturn.didMeta.encrypted;
decryptedProperties = JSON.parse(decryptedPayload);
for (propertyName in decryptedProperties){
switch (propertyName){
case 'private':
case 'public':
case 'symmetric':
var propertyValue = decryptedProperties[propertyName];
// anomolies in decoding of cryptokey
if (propertyValue.algorithm.name === "RSASSA-PKCS1-v1_5"){
var fixedAlg = {
name: propertyValue.algorithm.name,
hash: propertyValue.algorithm.hash.name
}
propertyValue.algorithm = fixedAlg;
}
if (propertyValue.isCryptoKey){
potentialError = 'db_encrypt: jwk_' + propertyName + ' import_key';
keyData = decryptedProperties['jwk_' + propertyName];
const result = await crypto.subtle.importKey(
'jwk',
keyData,
propertyValue.algorithm,
propertyValue.extractable,
propertyValue.usages
);
decryptedProperties[propertyName] = result;
}
break;
default:
break;
}
objectToReturn[propertyName] = decryptedProperties[propertyName];
}
}
return objectToReturn;
}
catch (err){
console.log("**********************************************");
console.log(potentialError);
console.log('propertyValue', propertyValue);
console.log('keyData', keyData);
console.log('error', err);
console.log("**********************************************");
}
}
Object.defineProperty(this, "clear", {
value: function(dbInfo){
return new Promise((resolve, reject) => {
for (var collectionName in dbInfo.collections){
// open a read/write db transaction, ready for clearing the data
var transaction = dbInfo.db.transaction([collectionName], "readwrite");
// report on the success of the transaction completing, when everything is done
transaction.oncomplete = function(event) {
resolve(true);
};
transaction.onerror = function(event) {
reject('Transaction not opened due to error: ' + transaction.error);
};
// create an object store on the transaction
var objectStore = transaction.objectStore(collectionName);
// Make a request to clear all the data out of the object store
var objectStoreRequest = objectStore.clear();
}
})
}
})
}