This repository was archived by the owner on Feb 8, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
update stellar-sdk and add keystore support for browser storage (#314) (
#315) * update stellar-sdk and add keystore support for browser storage * use current stellar sdk * pr comments * add types for browser storage
- Loading branch information
Showing
6 changed files
with
471 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { EncryptedKey } from "../types"; | ||
|
||
export interface BrowserStorageConfigParams { | ||
prefix?: string; | ||
storage: { | ||
get: (key?: string | string[] | {}) => Promise<{}>; | ||
remove: (key: string | string[]) => Promise<void>; | ||
set: (items: {}) => Promise<{}>; | ||
}; | ||
} | ||
|
||
const PREFIX = "stellarkeys"; | ||
|
||
/** | ||
* Facade for `BrowserStorageKeyStore` encapsulating the access to the actual | ||
* browser storage | ||
*/ | ||
export class BrowserStorageFacade { | ||
private storage: Storage | null; | ||
private prefix: string; | ||
|
||
constructor() { | ||
this.storage = null; | ||
this.prefix = PREFIX; | ||
} | ||
|
||
public configure(params: BrowserStorageConfigParams) { | ||
Object.assign(this, params); | ||
} | ||
|
||
public async hasKey(id: string) { | ||
this.check(); | ||
|
||
return this.storage !== null | ||
? !!Object.keys(await this.storage.get(`${this.prefix}:${id}`)).length | ||
: null; | ||
} | ||
|
||
public async getKey(id: string) { | ||
this.check(); | ||
const key = `${this.prefix}:${id}`; | ||
const itemObj = this.storage !== null ? await this.storage.get(key) : null; | ||
|
||
const item = itemObj[key]; | ||
return item || null; | ||
} | ||
|
||
public setKey(id: string, key: EncryptedKey) { | ||
this.check(); | ||
return this.storage !== null | ||
? this.storage.set({ [`${this.prefix}:${id}`]: { ...key } }) | ||
: null; | ||
} | ||
|
||
public removeKey(id: string) { | ||
this.check(); | ||
return this.storage !== null | ||
? this.storage.remove(`${this.prefix}:${id}`) | ||
: null; | ||
} | ||
|
||
public async getAllKeys() { | ||
this.check(); | ||
const regexp = RegExp(`^${PREFIX}\\:(.*)`); | ||
const keys: EncryptedKey[] = []; | ||
|
||
if (this.storage !== null) { | ||
const storageObj = await this.storage.get(null); | ||
const storageKeys = Object.keys(storageObj); | ||
for (const storageKey of storageKeys) { | ||
const raw_id = storageKey; | ||
if (raw_id !== null && regexp.test(raw_id)) { | ||
const key = await this.getKey(regexp.exec(raw_id)![1]); | ||
if (key !== null) { | ||
keys.push(key); | ||
} | ||
} | ||
} | ||
} | ||
return keys; | ||
} | ||
|
||
private check() { | ||
if (this.storage === null) { | ||
throw new Error("A storage object must have been set"); | ||
} | ||
if (this.prefix === "") { | ||
throw new Error("A non-empty prefix must have been set"); | ||
} | ||
return this.storage !== null && this.prefix !== ""; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import sinon from "sinon"; | ||
|
||
import { EncryptedKey } from "../types"; | ||
import { BrowserStorageKeyStore } from "./BrowserStorageKeyStore"; | ||
|
||
// tslint:disable-next-line | ||
describe("BrowserStorageKeyStore", function() { | ||
let clock: sinon.SinonFakeTimers; | ||
let testStore: BrowserStorageKeyStore; | ||
const encryptedKey: EncryptedKey = { | ||
id: "PURIFIER", | ||
encryptedBlob: "BLOB", | ||
encrypterName: "Test", | ||
salt: "SLFKJSDLKFJLSKDJFLKSJD", | ||
}; | ||
const keyMetadata = { | ||
id: "PURIFIER", | ||
}; | ||
const chrome = { | ||
storage: { | ||
local: { | ||
get: (_key?: string | string[] | {}) => Promise.resolve({}), | ||
set: (_items: {}) => Promise.resolve({}), | ||
remove: (_key: string | string[]) => Promise.resolve(), | ||
}, | ||
}, | ||
}; | ||
|
||
beforeEach(() => { | ||
clock = sinon.useFakeTimers(666); | ||
testStore = new BrowserStorageKeyStore(); | ||
|
||
testStore.configure({ storage: chrome.storage.local }); | ||
}); | ||
|
||
afterEach(() => { | ||
clock.restore(); | ||
sinon.restore(); | ||
}); | ||
|
||
it("properly stores keys", async () => { | ||
const chromeStorageLocalGetStub = sinon.stub(chrome.storage.local, "get"); | ||
|
||
/* first call returns empty to confirm keystore | ||
doesn't already exist before storing */ | ||
chromeStorageLocalGetStub.onCall(0).returns(Promise.resolve({})); | ||
const testMetadata = await testStore.storeKeys([encryptedKey]); | ||
|
||
expect(testMetadata).toEqual([keyMetadata]); | ||
|
||
// subsequent calls return the keystore as expected | ||
chromeStorageLocalGetStub.returns( | ||
Promise.resolve({ [`stellarkeys:${encryptedKey.id}`]: encryptedKey }), | ||
); | ||
const allKeys = await testStore.loadAllKeys(); | ||
|
||
expect(allKeys).toEqual([{ ...encryptedKey, ...keyMetadata }]); | ||
}); | ||
|
||
it("properly deletes keys", async () => { | ||
const chromeStorageLocalGetStub = sinon.stub(chrome.storage.local, "get"); | ||
|
||
/* first call returns empty to confirm keystore | ||
doesn't already exist before storing */ | ||
chromeStorageLocalGetStub.onCall(0).returns(Promise.resolve({})); | ||
await testStore.storeKeys([encryptedKey]); | ||
|
||
// subsequent calls return the keystore as expected | ||
chromeStorageLocalGetStub.returns( | ||
Promise.resolve({ [`stellarkeys:${encryptedKey.id}`]: encryptedKey }), | ||
); | ||
|
||
const allKeys = await testStore.loadAllKeys(); | ||
|
||
expect(allKeys).toEqual([{ ...encryptedKey, ...keyMetadata }]); | ||
|
||
const removalMetadata = await testStore.removeKey("PURIFIER"); | ||
chromeStorageLocalGetStub.returns(Promise.resolve({})); | ||
|
||
expect(removalMetadata).toEqual(keyMetadata); | ||
const noKeys = await testStore.loadAllKeys(); | ||
|
||
expect(noKeys).toEqual([]); | ||
}); | ||
|
||
it("passes PluginTesting", () => { | ||
/* | ||
TODO: | ||
this test cannot currently be run because we | ||
don't have an adequate way to stub chrome.local.storage yet */ | ||
// testKeyStore(testStore) | ||
// .then(() => { | ||
// done(); | ||
// }) | ||
// .catch(done); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { getKeyMetadata } from "../helpers/getKeyMetadata"; | ||
import { EncryptedKey, KeyMetadata, KeyStore } from "../types"; | ||
import { | ||
BrowserStorageConfigParams, | ||
BrowserStorageFacade, | ||
} from "./BrowserStorageFacade"; | ||
|
||
/** | ||
* KeyStore for Chrome and Firefox browser storage API: | ||
* https://developer.chrome.com/docs/extensions/reference/storage/. | ||
* Once instantiated and configured, pass it to the `KeyManager` contructor to | ||
* handle the storage of encrypted keys. | ||
* ```js | ||
* const browserKeyStore = new KeyManagerPlugins.BrowserStorageKeyStore(); | ||
* browserKeyStore.configure({ storage: chrome.storage.local }); | ||
* const keyManager = new KeyManager({ | ||
* keyStore: browserKeyStore | ||
* }); | ||
* ``` | ||
*/ | ||
export class BrowserStorageKeyStore implements KeyStore { | ||
public name: string; | ||
private keyStore: BrowserStorageFacade; | ||
|
||
constructor() { | ||
this.name = "BrowserStorageKeyStore"; | ||
this.keyStore = new BrowserStorageFacade(); | ||
} | ||
|
||
/** | ||
* The configuration is where the storage engine is set up and configured. | ||
* It must follow the Storage interface : | ||
* https://developer.chrome.com/docs/extensions/reference/storage/). | ||
* This is mostly for use with Chrome and Firefox storage in addition to | ||
* libraries that shim this (for ex: webextension-polyfill) | ||
* @param {BrowserStorageConfigParams} params A configuration object. | ||
* @param {Storage} params.storage The Storage instance. Required. | ||
* @param {string} [params.prefix] The prefix for the names in the storage. | ||
* @return {Promise} | ||
*/ | ||
public configure(params: BrowserStorageConfigParams) { | ||
try { | ||
this.keyStore.configure(params); | ||
return Promise.resolve(); | ||
} catch (e) { | ||
return Promise.reject(e); | ||
} | ||
} | ||
|
||
public async storeKeys(keys: EncryptedKey[]) { | ||
// We can't store keys if they're already there | ||
const usedKeys: EncryptedKey[] = []; | ||
|
||
for (const encryptedKey of keys) { | ||
const hasKey = await this.keyStore.hasKey(encryptedKey.id); | ||
if (hasKey) { | ||
usedKeys.push(encryptedKey); | ||
} | ||
} | ||
|
||
if (usedKeys.length) { | ||
return Promise.reject( | ||
`Some keys were already stored in the keystore: ${usedKeys | ||
.map((k) => k.id) | ||
.join(", ")}`, | ||
); | ||
} | ||
|
||
const keysMetadata: KeyMetadata[] = []; | ||
|
||
for (const encryptedKey of keys) { | ||
this.keyStore.setKey(encryptedKey.id, encryptedKey); | ||
keysMetadata.push(getKeyMetadata(encryptedKey)); | ||
} | ||
|
||
return Promise.resolve(keysMetadata); | ||
} | ||
|
||
public updateKeys(keys: EncryptedKey[]) { | ||
// we can't update keys if they're already stored | ||
const invalidKeys: EncryptedKey[] = keys.filter( | ||
async (encryptedKey: EncryptedKey) => | ||
!(await this.keyStore.hasKey(encryptedKey.id)), | ||
); | ||
|
||
if (invalidKeys.length) { | ||
return Promise.reject( | ||
`Some keys couldn't be found in the keystore: ${invalidKeys | ||
.map((k) => k.id) | ||
.join(", ")}`, | ||
); | ||
} | ||
|
||
const keysMetadata = keys.map((encryptedKey: EncryptedKey) => { | ||
this.keyStore.setKey(encryptedKey.id, encryptedKey); | ||
return getKeyMetadata(encryptedKey); | ||
}); | ||
|
||
return Promise.resolve(keysMetadata); | ||
} | ||
|
||
public async loadKey(id: string) { | ||
const key = await this.keyStore.getKey(id); | ||
if (!key) { | ||
return Promise.reject(id); | ||
} | ||
return Promise.resolve(key); | ||
} | ||
|
||
public async removeKey(id: string) { | ||
if (!this.keyStore.hasKey(id)) { | ||
return Promise.reject(id); | ||
} | ||
|
||
const key = await this.keyStore.getKey(id); | ||
const metadata: KeyMetadata = getKeyMetadata(key); | ||
this.keyStore.removeKey(id); | ||
|
||
return Promise.resolve(metadata); | ||
} | ||
|
||
public async loadAllKeys() { | ||
const keys = await this.keyStore.getAllKeys(); | ||
return Promise.resolve(keys); | ||
} | ||
} |
Oops, something went wrong.