src/security/engines/BrowserKeyStore.js
const MEMBER_KEY_DB = 'member_key';
const MEMBER_KEY_DB_VERSION = 1;
const MEMBER_KEY_STORE = 'member_keys';
const READ_ONLY = 'readonly';
const READ_WRITE = 'readwrite';
class BrowserKeyStore {
/**
* Keep track of the ID of the most recently active member.
*
* @param {string} memberId - ID of member
*/
static setActiveMemberId(memberId) {
localStorage.activeMemberId = memberId;
}
/**
* Get the ID of the most recently active member.
*
* @return {string} ID of member
*/
static getActiveMemberId() {
const memberId = localStorage.activeMemberId;
if (!memberId) {
throw new Error('No active memberId on this browser');
}
return memberId;
}
/**
* Store a member's key pair.
*
* @param {string} memberId - ID of member
* @param {Object} keyPair - key pair to store
* @return {Promise} promise that resolves into the key pair that was passed in
*/
async put(memberId, keyPair) {
if (!memberId) {
throw new Error('Invalid memberId');
}
if (!keyPair) {
throw new Error('Don\'t know what key to put');
}
if (!keyPair.level) {
throw new Error('Don\'t know what level to put key');
}
if (!BROWSER) {
throw new Error('Browser Only');
}
if (keyPair.expiresAtMs < Date.now()) {
throw new Error(`Key ${keyPair.id} has expired`);
}
const store = await BrowserKeyStore._getObjectStore(MEMBER_KEY_STORE, READ_WRITE);
return new Promise((resolve, reject) => {
const getReq = store.get(memberId);
getReq.onsuccess = () => {
const member = getReq.result || {};
const putReq = store.put(
Object.assign(member, {[keyPair.level]: keyPair}),
memberId);
putReq.onsuccess = () => {
BrowserKeyStore.setActiveMemberId(memberId);
resolve(keyPair);
};
putReq.onerror = () =>
reject(new Error(`Error saving member to database: ${putReq.error}`));
};
getReq.onerror = () =>
reject(new Error(`Error getting member from database: ${getReq.error}`));
});
}
/**
* Look up a key by memberId and level.
*
* @param {string} memberId - ID of member
* @param {string} level - 'LOW', 'STANDARD', or 'PRIVILEGED'
* @return {Promise} promise that resolves into the retrieved key pair
*/
async getByLevel(memberId, level) {
if (!memberId) {
throw new Error('Invalid memberId');
}
if (!level) {
throw new Error('Don\'t know what key level to get');
}
if (!BROWSER) {
throw new Error('Browser Only');
}
const store = await BrowserKeyStore._getObjectStore(MEMBER_KEY_STORE);
return new Promise((resolve, reject) => {
const getReq = store.get(memberId);
getReq.onsuccess = () => {
const member = getReq.result;
if (!member) {
return reject(new Error(`Member with id ${memberId} not found`));
}
if (!member[level]) {
return reject(new Error(`No key with level ${level} found`));
}
if (member[level].expiresAtMs < Date.now()) {
return reject(new Error(`Key with level ${level} has expired`));
}
BrowserKeyStore.setActiveMemberId(memberId);
resolve(getReq.result[level]);
};
getReq.onerror = () =>
reject(new Error(`Error getting member from database: ${getReq.error}`));
});
}
/**
* Look up a key by memberId and keyId.
*
* @param {string} memberId - ID of member
* @param {string} keyId - key ID
* @return {Promise} promise that resolves into the retrieved key pair
*/
async getById(memberId, keyId) {
if (!memberId) {
throw new Error('Invalid memberId');
}
if (!keyId) {
throw new Error('Don\'t know id of key to get');
}
if (!BROWSER) {
throw new Error('Browser Only');
}
const store = await BrowserKeyStore._getObjectStore(MEMBER_KEY_STORE);
return new Promise((resolve, reject) => {
const getReq = store.get(memberId);
getReq.onsuccess = () => {
const member = getReq.result;
if (!member) {
return reject(new Error(`member ${memberId} not found`));
}
Object.values(member).forEach(keyPair => {
if (keyPair.id === keyId) {
if (keyPair.expiresAtMs < Date.now()) {
reject(new Error(`Key with id ${keyPair.id} has expired`));
}
BrowserKeyStore.setActiveMemberId(memberId);
resolve(keyPair);
}
});
reject(new Error(`No key with id ${keyId} found`));
};
getReq.onerror = () =>
reject(new Error(`Error getting member from database: ${getReq.error}`));
});
}
/**
* Return list of member's keys.
*
* @param {string} memberId - ID of member
* @return {Promise} promise that resolves into the retrieved list of key pairs
*/
async listKeys(memberId) {
if (!memberId) {
throw new Error('Invalid memberId');
}
if (!BROWSER) {
throw new Error('Browser Only');
}
const store = await BrowserKeyStore._getObjectStore(MEMBER_KEY_STORE);
return new Promise((resolve, reject) => {
const getReq = store.get(memberId);
getReq.onsuccess = () => {
const member = getReq.result;
if (!member) {
return reject(new Error(`member ${memberId} not found`));
}
BrowserKeyStore.setActiveMemberId(memberId);
resolve(Object.values(member)
.filter(keyPair => !(keyPair.expiresAtMs < Date.now())));
};
getReq.onerror = () =>
reject(new Error(`Error getting member from database: ${getReq.error}`));
});
}
/**
* Clears all keys in object store
*
* @return {Promise<any>} promise that resolves when all keys have been cleared
*/
async clearAllKeys() {
const store = await BrowserKeyStore._getObjectStore(MEMBER_KEY_STORE, READ_WRITE);
return new Promise((resolve, reject) => {
const clearReq = store.clear();
clearReq.onsuccess = () => resolve();
clearReq.onerror = () =>
reject(new Error(`Error clearing the database: ${clearReq.error}`));
});
}
/**
* Opens an instance of IndexedDB
*
* @param {string} dbName - name of db
* @param {string} dbVersion - version of db
* @return {Promise<IDBDatabase>} promise that resolves into the database object
* @private
*/
static async _openDb(dbName, dbVersion) {
return new Promise((resolve, reject) => {
if (!indexedDB) reject(new Error('Your browser does not support IndexedDB'));
const openReq = indexedDB.open(dbName, dbVersion);
openReq.onsuccess = () => {
resolve(openReq.result);
};
openReq.onerror = () => reject(new Error(`Error opening database: ${openReq.error}`));
openReq.onupgradeneeded = e => {
const db = e.target.result;
db.createObjectStore(MEMBER_KEY_STORE);
};
});
}
/**
* Retrieves an object store from the db
*
* @param {string} storeName - name of object store
* @param {string} mode - readonly, readwrite, or readwriteflush, defaults to readonly
* @return {Promise<IDBObjectStore>} promise that resolves into the store object
* @private
*/
static async _getObjectStore(storeName, mode = READ_ONLY) {
const db = await BrowserKeyStore._openDb(MEMBER_KEY_DB, MEMBER_KEY_DB_VERSION);
const txn = db.transaction(storeName, mode);
txn.oncomplete = () => db.close();
txn.onerror = () => {
throw new Error(`Error opening transaction: ${txn.error}`);
};
return txn.objectStore(storeName);
}
}
export default BrowserKeyStore;