Skip to content
This repository was archived by the owner on Feb 8, 2024. It is now read-only.

Commit

Permalink
update stellar-sdk and add keystore support for browser storage (#314) (
Browse files Browse the repository at this point in the history
#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
piyalbasu authored Dec 1, 2022
1 parent f3b5a3e commit 65feb44
Show file tree
Hide file tree
Showing 6 changed files with 471 additions and 77 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stellar/wallet-sdk",
"version": "0.7.0-rc.0",
"version": "0.8.0",
"description": "Libraries to help you write Stellar-enabled wallets in Javascript",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -67,7 +67,7 @@
"prettier": "^1.17.0",
"regenerator-runtime": "^0.13.3",
"sinon": "^7.3.1",
"stellar-sdk": "^9.0.1",
"stellar-sdk": "^10.4.0",
"terser-webpack-plugin": "^2.3.0",
"ts-loader": "^6.2.1",
"tsc-watch": "^2.1.2",
Expand Down
2 changes: 2 additions & 0 deletions src/KeyManagerPlugins.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { BrowserStorageKeyStore } from "./plugins/BrowserStorageKeyStore";
import { IdentityEncrypter } from "./plugins/IdentityEncrypter";
import { LocalStorageKeyStore } from "./plugins/LocalStorageKeyStore";
import { MemoryKeyStore } from "./plugins/MemoryKeyStore";
import { ScryptEncrypter } from "./plugins/ScryptEncrypter";

export const KeyManagerPlugins: any = {
BrowserStorageKeyStore,
IdentityEncrypter,
MemoryKeyStore,
LocalStorageKeyStore,
Expand Down
92 changes: 92 additions & 0 deletions src/plugins/BrowserStorageFacade.ts
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 !== "";
}
}
97 changes: 97 additions & 0 deletions src/plugins/BrowserStorageKeyStore.test.ts
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);
});
});
126 changes: 126 additions & 0 deletions src/plugins/BrowserStorageKeyStore.ts
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);
}
}
Loading

0 comments on commit 65feb44

Please sign in to comment.