From 56433d96d857ce5e707a055b73aac8bc78167d1b Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Tue, 2 Aug 2022 14:49:48 -0700 Subject: [PATCH 1/6] feat: adds IdbStorage as new default for auth-client --- package-lock.json | 201 ++++++++++++++++++++++++++++- packages/auth-client/package.json | 4 +- packages/auth-client/src/index.ts | 87 ++++++++++++- packages/auth-client/test-setup.ts | 5 +- 4 files changed, 292 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d6f8fe884..2d89fb0a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4724,7 +4724,8 @@ }, "node_modules/@peculiar/webcrypto": { "version": "1.4.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.0.tgz", + "integrity": "sha512-U58N44b2m3OuTgpmKgf0LPDOmP3bhwNz01vAnj1mBwxBASRhptWYK+M3zG+HBkDqGQM+bFsoIihTW8MdmPXEqg==", "dependencies": { "@peculiar/asn1-schema": "^2.1.6", "@peculiar/json-schema": "^1.1.12", @@ -6474,6 +6475,15 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-arraybuffer-es6": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz", + "integrity": "sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "funding": [ @@ -9312,6 +9322,15 @@ "node": "> 0.1.90" } }, + "node_modules/fake-indexeddb": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-4.0.0.tgz", + "integrity": "sha512-oCfWSJ/qvQn1XPZ8SHX6kY3zr1t+bN7faZ/lltGY0SBGhFOPXnWf0+pbO/MOAgfMx6khC2gK3S/bvAgQpuQHDQ==", + "dev": true, + "dependencies": { + "realistic-structured-clone": "^3.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -15207,6 +15226,32 @@ "dev": true, "license": "WTFPL" }, + "node_modules/realistic-structured-clone": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/realistic-structured-clone/-/realistic-structured-clone-3.0.0.tgz", + "integrity": "sha512-rOjh4nuWkAqf9PWu6JVpOWD4ndI+JHfgiZeMmujYcPi+fvILUu7g6l26TC1K5aBIp34nV+jE1cDO75EKOfHC5Q==", + "dev": true, + "dependencies": { + "domexception": "^1.0.1", + "typeson": "^6.1.0", + "typeson-registry": "^1.0.0-alpha.20" + } + }, + "node_modules/realistic-structured-clone/node_modules/domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "dependencies": { + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/realistic-structured-clone/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, "node_modules/rechoir": { "version": "0.7.1", "license": "MIT", @@ -16804,6 +16849,64 @@ "node": ">=4.2.0" } }, + "node_modules/typeson": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/typeson/-/typeson-6.1.0.tgz", + "integrity": "sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==", + "dev": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/typeson-registry": { + "version": "1.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/typeson-registry/-/typeson-registry-1.0.0-alpha.39.tgz", + "integrity": "sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw==", + "dev": true, + "dependencies": { + "base64-arraybuffer-es6": "^0.7.0", + "typeson": "^6.0.0", + "whatwg-url": "^8.4.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/typeson-registry/node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/typeson-registry/node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true, + "engines": { + "node": ">=10.4" + } + }, + "node_modules/typeson-registry/node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/unbox-primitive": { "version": "1.0.1", "license": "MIT", @@ -17980,16 +18083,19 @@ "license": "Apache-2.0", "dependencies": { "@types/jest": "^28.1.4", + "idb-keyval": "^6.2.0", "jest": "^28.1.2", "ts-jest": "^28.0.5", "ts-node": "^10.8.2" }, "devDependencies": { + "@peculiar/webcrypto": "^1.4.0", "@trust/webcrypto": "^0.9.2", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "eslint": "^8.19.0", "eslint-plugin-jsdoc": "^39.3.3", + "fake-indexeddb": "^4.0.0", "jest-environment-jsdom": "^28.1.2", "text-encoding": "^0.7.0", "tslint": "^5.20.0", @@ -19662,12 +19768,15 @@ "@dfinity/auth-client": { "version": "file:packages/auth-client", "requires": { + "@peculiar/webcrypto": "*", "@trust/webcrypto": "^0.9.2", "@types/jest": "^28.1.4", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "eslint": "^8.19.0", "eslint-plugin-jsdoc": "^39.3.3", + "fake-indexeddb": "^4.0.0", + "idb-keyval": "^6.2.0", "jest": "^28.1.2", "jest-environment-jsdom": "^28.1.2", "text-encoding": "^0.7.0", @@ -22073,6 +22182,8 @@ }, "@peculiar/webcrypto": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.0.tgz", + "integrity": "sha512-U58N44b2m3OuTgpmKgf0LPDOmP3bhwNz01vAnj1mBwxBASRhptWYK+M3zG+HBkDqGQM+bFsoIihTW8MdmPXEqg==", "requires": { "@peculiar/asn1-schema": "^2.1.6", "@peculiar/json-schema": "^1.1.12", @@ -23332,6 +23443,12 @@ "base64-arraybuffer": { "version": "0.2.0" }, + "base64-arraybuffer-es6": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz", + "integrity": "sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==", + "dev": true + }, "base64-js": { "version": "1.5.1" }, @@ -25259,6 +25376,15 @@ "version": "0.1.8", "dev": true }, + "fake-indexeddb": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-4.0.0.tgz", + "integrity": "sha512-oCfWSJ/qvQn1XPZ8SHX6kY3zr1t+bN7faZ/lltGY0SBGhFOPXnWf0+pbO/MOAgfMx6khC2gK3S/bvAgQpuQHDQ==", + "dev": true, + "requires": { + "realistic-structured-clone": "^3.0.0" + } + }, "fast-deep-equal": { "version": "3.1.3" }, @@ -29130,6 +29256,34 @@ "version": "1.2.0", "dev": true }, + "realistic-structured-clone": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/realistic-structured-clone/-/realistic-structured-clone-3.0.0.tgz", + "integrity": "sha512-rOjh4nuWkAqf9PWu6JVpOWD4ndI+JHfgiZeMmujYcPi+fvILUu7g6l26TC1K5aBIp34nV+jE1cDO75EKOfHC5Q==", + "dev": true, + "requires": { + "domexception": "^1.0.1", + "typeson": "^6.1.0", + "typeson-registry": "^1.0.0-alpha.20" + }, + "dependencies": { + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + } + } + }, "rechoir": { "version": "0.7.1", "requires": { @@ -30142,6 +30296,51 @@ "typescript": { "version": "4.7.4" }, + "typeson": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/typeson/-/typeson-6.1.0.tgz", + "integrity": "sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==", + "dev": true + }, + "typeson-registry": { + "version": "1.0.0-alpha.39", + "resolved": "https://registry.npmjs.org/typeson-registry/-/typeson-registry-1.0.0-alpha.39.tgz", + "integrity": "sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw==", + "dev": true, + "requires": { + "base64-arraybuffer-es6": "^0.7.0", + "typeson": "^6.0.0", + "whatwg-url": "^8.4.0" + }, + "dependencies": { + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + } + } + }, "unbox-primitive": { "version": "1.0.1", "requires": { diff --git a/packages/auth-client/package.json b/packages/auth-client/package.json index e59f2be5f..b1b087a75 100644 --- a/packages/auth-client/package.json +++ b/packages/auth-client/package.json @@ -51,11 +51,12 @@ "@dfinity/principal": "^0.12.2" }, "devDependencies": { - "@trust/webcrypto": "^0.9.2", + "@peculiar/webcrypto": "^1.4.0", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "eslint": "^8.19.0", "eslint-plugin-jsdoc": "^39.3.3", + "fake-indexeddb": "^4.0.0", "jest-environment-jsdom": "^28.1.2", "text-encoding": "^0.7.0", "tslint": "^5.20.0", @@ -65,6 +66,7 @@ }, "dependencies": { "@types/jest": "^28.1.4", + "idb-keyval": "^6.2.0", "jest": "^28.1.2", "ts-jest": "^28.0.5", "ts-node": "^10.8.2" diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index efc25cf7d..2d5a36535 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -15,6 +15,7 @@ import { } from '@dfinity/identity'; import { Principal } from '@dfinity/principal'; import { IdleManager, IdleManagerOptions } from './idleManager'; +import { get, set, del } from 'idb-keyval'; const KEY_LOCALSTORAGE_KEY = 'identity'; const KEY_LOCALSTORAGE_DELEGATION = 'delegation'; @@ -166,6 +167,80 @@ export class LocalStorage implements AuthClientStorage { } } +export class IdbStorage implements AuthClientStorage { + private get encryptKey() { + return new Promise(resolve => { + get('ic-auth-key').then(async storedKey => { + const key = + storedKey ?? + (await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + false, + ['encrypt', 'decrypt'], + )); + + await set('ic-auth-key', key); + resolve(key); + }); + }); + } + private get iv() { + return new Promise(resolve => { + get('ic-iv').then(async storedIv => { + const iv = storedIv ?? (await crypto.getRandomValues(new Uint8Array(16))); + + await set('ic-iv', iv); + resolve(iv); + }); + }); + } + + constructor(public readonly prefix = 'ic-') { + console.trace('idb-load'); + } + + public async get(key: string): Promise { + const encryptKey = await await this.encryptKey; + const encrypted = await get(this.prefix + key); + if (encrypted) { + const decoder = new TextDecoder(); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-CBC', iv: await this.iv }, + encryptKey, + encrypted, + ); + return decoder.decode(decrypted); + } + return null; + } + + public async set(key: string, value: string): Promise { + const encoder = new TextEncoder(); + const encryptKey = await await this.encryptKey; + + try { + await set( + this.prefix + key, + await crypto.subtle.encrypt( + { name: 'AES-CBC', iv: await this.iv }, + encryptKey, + encoder.encode(value).buffer, + ), + ); + } catch (error) { + console.error(error); + } + } + + public async remove(key: string): Promise { + await del(this.prefix + key); + } +} + interface AuthReadyMessage { kind: 'authorize-ready'; } @@ -234,20 +309,26 @@ export class AuthClient { idleOptions?: IdleOptions; } = {}, ): Promise { - const storage = options.storage ?? new LocalStorage('ic-'); + const storage = options.storage ?? new IdbStorage('ic-'); let key: null | SignIdentity = null; if (options.identity) { + console.log('option identity'); + key = options.identity; } else { const maybeIdentityStorage = await storage.get(KEY_LOCALSTORAGE_KEY); + console.log('stored identity', maybeIdentityStorage); if (maybeIdentityStorage) { try { key = Ed25519KeyIdentity.fromJSON(maybeIdentityStorage); } catch (e) { + console.error(e); // Ignore this, this means that the localStorage value isn't a valid Ed25519KeyIdentity // serialization. } + } else { + alert('no identity found'); } } @@ -485,12 +566,16 @@ export class AuthClient { // it a sync function. Having _handleSuccess as an async function // messes up the jest tests for some reason. if (this._chain) { + console.log('chain', this._chain); + console.log(this._storage); await this._storage.set( KEY_LOCALSTORAGE_DELEGATION, JSON.stringify(this._chain.toJSON()), ); + console.log(await this._storage.get(KEY_LOCALSTORAGE_DELEGATION)); } } catch (err) { + console.error(err); this._handleFailure((err as Error).message, options?.onError); } break; diff --git a/packages/auth-client/test-setup.ts b/packages/auth-client/test-setup.ts index ec0be32c6..8562441ac 100644 --- a/packages/auth-client/test-setup.ts +++ b/packages/auth-client/test-setup.ts @@ -6,8 +6,9 @@ // // Note that we can use webpack configuration to make some features available to // Node.js in a similar way. - -global.crypto = require('@trust/webcrypto'); +import { Crypto } from '@peculiar/webcrypto'; +global.crypto = new Crypto(); +import 'fake-indexeddb/auto'; global.TextEncoder = require('text-encoding').TextEncoder; global.TextDecoder = require('text-encoding').TextDecoder; require('whatwg-fetch'); From 06a7808f7b81aa1301ec77647793e335bd7a14b4 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Tue, 2 Aug 2022 16:54:25 -0700 Subject: [PATCH 2/6] using idb library for version --- package-lock.json | 18 +++++++++---- packages/auth-client/package.json | 2 +- packages/auth-client/src/db.ts | 45 +++++++++++++++++++++++++++++++ packages/auth-client/src/index.ts | 43 +++++++++++------------------ 4 files changed, 75 insertions(+), 33 deletions(-) create mode 100644 packages/auth-client/src/db.ts diff --git a/package-lock.json b/package-lock.json index 2d89fb0a5..0fd89ee38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10290,6 +10290,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.2.tgz", + "integrity": "sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg==" + }, "node_modules/idb-keyval": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz", @@ -18083,14 +18088,13 @@ "license": "Apache-2.0", "dependencies": { "@types/jest": "^28.1.4", - "idb-keyval": "^6.2.0", + "idb": "^7.0.2", "jest": "^28.1.2", "ts-jest": "^28.0.5", "ts-node": "^10.8.2" }, "devDependencies": { "@peculiar/webcrypto": "^1.4.0", - "@trust/webcrypto": "^0.9.2", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "eslint": "^8.19.0", @@ -19768,15 +19772,14 @@ "@dfinity/auth-client": { "version": "file:packages/auth-client", "requires": { - "@peculiar/webcrypto": "*", - "@trust/webcrypto": "^0.9.2", + "@peculiar/webcrypto": "^1.4.0", "@types/jest": "^28.1.4", "@typescript-eslint/eslint-plugin": "^5.30.5", "@typescript-eslint/parser": "^5.30.5", "eslint": "^8.19.0", "eslint-plugin-jsdoc": "^39.3.3", "fake-indexeddb": "^4.0.0", - "idb-keyval": "^6.2.0", + "idb": "^7.0.2", "jest": "^28.1.2", "jest-environment-jsdom": "^28.1.2", "text-encoding": "^0.7.0", @@ -26053,6 +26056,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "idb": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.2.tgz", + "integrity": "sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg==" + }, "idb-keyval": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz", diff --git a/packages/auth-client/package.json b/packages/auth-client/package.json index b1b087a75..720d05817 100644 --- a/packages/auth-client/package.json +++ b/packages/auth-client/package.json @@ -66,7 +66,7 @@ }, "dependencies": { "@types/jest": "^28.1.4", - "idb-keyval": "^6.2.0", + "idb": "^7.0.2", "jest": "^28.1.2", "ts-jest": "^28.0.5", "ts-node": "^10.8.2" diff --git a/packages/auth-client/src/db.ts b/packages/auth-client/src/db.ts new file mode 100644 index 000000000..31ba4bb69 --- /dev/null +++ b/packages/auth-client/src/db.ts @@ -0,0 +1,45 @@ +import { openDB, IDBPDatabase } from 'idb'; + +export type Database = Promise>; + +export const openDbStore = async () => + await openDB('auth-client-db', 1, { + upgrade: database => { + if (database.objectStoreNames.contains('ic-idp')) { + database.clear('ic-idp'); + } + database.createObjectStore('ic-idp'); + }, + }); + +/** + * + * @param db database + * @param key string + * @returns + */ +export async function getValue(db: Database, key: string): Promise { + return (await db).get('ic-idp', key); +} + +/** + * + * @param db database + * @param value any value to set + * @param key string asdf + */ +export async function setValue(db: Database, value: T, key: string): Promise { + (await db).put('ic-idp', value, key); +} + +/** + * + * @param db database + * @param value any value to remove + * @param key string asdf + */ +export async function removeValue(db: Database, key: string): Promise { + (await db).delete('ic-idp', key); +} + +export const db = openDbStore(); diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 2d5a36535..9dcef9804 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -14,11 +14,14 @@ import { Ed25519KeyIdentity, } from '@dfinity/identity'; import { Principal } from '@dfinity/principal'; +import { db, getValue, removeValue, setValue } from './db'; import { IdleManager, IdleManagerOptions } from './idleManager'; -import { get, set, del } from 'idb-keyval'; +const set = async (key: string, value: T) => await setValue(db, value, key); +const get = async (key: string): Promise => await getValue(db, key); +const remove = async (key: string) => await removeValue(db, key); -const KEY_LOCALSTORAGE_KEY = 'identity'; -const KEY_LOCALSTORAGE_DELEGATION = 'delegation'; +const KEY_STORAGE_KEY = 'identity'; +const KEY_STORAGE_DELEGATION = 'delegation'; const IDENTITY_PROVIDER_DEFAULT = 'https://identity.ic0.app'; const IDENTITY_PROVIDER_ENDPOINT = '#authorize'; @@ -124,8 +127,9 @@ interface InternetIdentityAuthResponseSuccess { } async function _deleteStorage(storage: AuthClientStorage) { - await storage.remove(KEY_LOCALSTORAGE_KEY); - await storage.remove(KEY_LOCALSTORAGE_DELEGATION); + await storage.remove(KEY_STORAGE_KEY); + await storage.remove(KEY_STORAGE_DELEGATION); + await storage.remove(KEY_STORAGE_DELEGATION); } export class LocalStorage implements AuthClientStorage { @@ -198,13 +202,11 @@ export class IdbStorage implements AuthClientStorage { }); } - constructor(public readonly prefix = 'ic-') { - console.trace('idb-load'); - } + constructor(public readonly prefix = 'ic-') {} public async get(key: string): Promise { const encryptKey = await await this.encryptKey; - const encrypted = await get(this.prefix + key); + const encrypted = await get(this.prefix + key); if (encrypted) { const decoder = new TextDecoder(); @@ -237,7 +239,7 @@ export class IdbStorage implements AuthClientStorage { } public async remove(key: string): Promise { - await del(this.prefix + key); + await remove(this.prefix + key); } } @@ -313,22 +315,16 @@ export class AuthClient { let key: null | SignIdentity = null; if (options.identity) { - console.log('option identity'); - key = options.identity; } else { - const maybeIdentityStorage = await storage.get(KEY_LOCALSTORAGE_KEY); - console.log('stored identity', maybeIdentityStorage); + const maybeIdentityStorage = await storage.get(KEY_STORAGE_KEY); if (maybeIdentityStorage) { try { key = Ed25519KeyIdentity.fromJSON(maybeIdentityStorage); } catch (e) { - console.error(e); // Ignore this, this means that the localStorage value isn't a valid Ed25519KeyIdentity // serialization. } - } else { - alert('no identity found'); } } @@ -337,7 +333,7 @@ export class AuthClient { if (key) { try { - const chainStorage = await storage.get(KEY_LOCALSTORAGE_DELEGATION); + const chainStorage = await storage.get(KEY_STORAGE_DELEGATION); if (options.identity) { identity = options.identity; @@ -490,7 +486,7 @@ export class AuthClient { // Create a new key (whether or not one was in storage). key = Ed25519KeyIdentity.generate(); this._key = key; - await this._storage.set(KEY_LOCALSTORAGE_KEY, JSON.stringify(key)); + await this._storage.set(KEY_STORAGE_KEY, JSON.stringify(key)); } // Set default maxTimeToLive to 8 hours @@ -566,16 +562,9 @@ export class AuthClient { // it a sync function. Having _handleSuccess as an async function // messes up the jest tests for some reason. if (this._chain) { - console.log('chain', this._chain); - console.log(this._storage); - await this._storage.set( - KEY_LOCALSTORAGE_DELEGATION, - JSON.stringify(this._chain.toJSON()), - ); - console.log(await this._storage.get(KEY_LOCALSTORAGE_DELEGATION)); + await this._storage.set(KEY_STORAGE_DELEGATION, JSON.stringify(this._chain.toJSON())); } } catch (err) { - console.error(err); this._handleFailure((err as Error).message, options?.onError); } break; From 24eae734c3621f7b40a114772d53b464a4ef60a5 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Wed, 3 Aug 2022 17:22:56 -0700 Subject: [PATCH 3/6] refactor to separate encryptedIdb and simple Idb storage --- packages/auth-client/src/db.test.ts | 18 +++ packages/auth-client/src/db.ts | 17 +-- packages/auth-client/src/index.test.ts | 18 ++- packages/auth-client/src/index.ts | 153 +++---------------------- packages/auth-client/src/storage.ts | 145 +++++++++++++++++++++++ packages/auth-client/test-setup.ts | 1 - 6 files changed, 208 insertions(+), 144 deletions(-) create mode 100644 packages/auth-client/src/db.test.ts create mode 100644 packages/auth-client/src/storage.ts diff --git a/packages/auth-client/src/db.test.ts b/packages/auth-client/src/db.test.ts new file mode 100644 index 000000000..b8eee3a38 --- /dev/null +++ b/packages/auth-client/src/db.test.ts @@ -0,0 +1,18 @@ +import { db, getValue, removeValue, setValue } from './db'; + +describe('indexeddb wrapper', () => { + it('should store a basic key value', async () => { + const shouldSet = async () => await setValue(db, 'testValue', 'testKey'); + expect(shouldSet).not.toThrow(); + + expect(await getValue(db, 'testKey')).toBe('testValue'); + }); + it('should support removing a value', async () => { + await setValue(db, 'testValue', 'testKey'); + expect(await getValue(db, 'testKey')).toBe('testValue'); + + await removeValue(db, 'testKey'); + + expect(await getValue(db, 'testKey')).toBe(undefined); + }); +}); diff --git a/packages/auth-client/src/db.ts b/packages/auth-client/src/db.ts index 31ba4bb69..d8a69cbff 100644 --- a/packages/auth-client/src/db.ts +++ b/packages/auth-client/src/db.ts @@ -1,14 +1,17 @@ import { openDB, IDBPDatabase } from 'idb'; export type Database = Promise>; +export const AUTH_DB_NAME = 'auth-client-db'; +export const OBJECT_STORE_NAME = 'keyval-store'; export const openDbStore = async () => - await openDB('auth-client-db', 1, { + await openDB(AUTH_DB_NAME, 1, { upgrade: database => { - if (database.objectStoreNames.contains('ic-idp')) { - database.clear('ic-idp'); + database.objectStoreNames; + if (database.objectStoreNames.contains(OBJECT_STORE_NAME)) { + database.clear(OBJECT_STORE_NAME); } - database.createObjectStore('ic-idp'); + database.createObjectStore(OBJECT_STORE_NAME); }, }); @@ -19,7 +22,7 @@ export const openDbStore = async () => * @returns */ export async function getValue(db: Database, key: string): Promise { - return (await db).get('ic-idp', key); + return (await db).get(OBJECT_STORE_NAME, key); } /** @@ -29,7 +32,7 @@ export async function getValue(db: Database, key: string): Promise(db: Database, value: T, key: string): Promise { - (await db).put('ic-idp', value, key); + (await db).put(OBJECT_STORE_NAME, value, key); } /** @@ -39,7 +42,7 @@ export async function setValue(db: Database, value: T, key: string): Promise< * @param key string asdf */ export async function removeValue(db: Database, key: string): Promise { - (await db).delete('ic-idp', key); + (await db).delete(OBJECT_STORE_NAME, key); } export const db = openDbStore(); diff --git a/packages/auth-client/src/index.test.ts b/packages/auth-client/src/index.test.ts index d73ab8ae9..46a3bdd19 100644 --- a/packages/auth-client/src/index.test.ts +++ b/packages/auth-client/src/index.test.ts @@ -1,9 +1,10 @@ +import 'fake-indexeddb/auto'; import { Actor, HttpAgent } from '@dfinity/agent'; import { AgentError } from '@dfinity/agent/lib/cjs/errors'; import { IDL } from '@dfinity/candid'; import { Ed25519KeyIdentity } from '@dfinity/identity'; import { Principal } from '@dfinity/principal'; -import { AuthClient, AuthClientStorage, ERROR_USER_INTERRUPT } from './index'; +import { AuthClient, ERROR_USER_INTERRUPT, IdbStorage } from './index'; /** * A class for mocking the IDP service. @@ -321,6 +322,21 @@ describe('Auth Client', () => { }); }); +describe('IdbStorage', () => { + it('should handle get and set', async () => { + const storage = new IdbStorage(); + + await storage.set('testKey', 'testValue'); + expect(await storage.get('testKey')).toBe('testValue'); + }); +}); + +describe('EncryptedIdbStorage', () => { + it.skip('should handle get and set', async () => { + // The CryptoKey does not currently interact correctly with the fake-indexeddb mock + }); +}); + // A minimal interface of our interactions with the Window object of the IDP. interface IdpWindow { postMessage(message: { kind: string }): void; diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 9dcef9804..5699189cc 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -14,14 +14,18 @@ import { Ed25519KeyIdentity, } from '@dfinity/identity'; import { Principal } from '@dfinity/principal'; -import { db, getValue, removeValue, setValue } from './db'; import { IdleManager, IdleManagerOptions } from './idleManager'; -const set = async (key: string, value: T) => await setValue(db, value, key); -const get = async (key: string): Promise => await getValue(db, key); -const remove = async (key: string) => await removeValue(db, key); +import { + AuthClientStorage, + IdbStorage, + KEY_ENCRYPTION, + KEY_STORAGE_DELEGATION, + KEY_STORAGE_KEY, + KEY_VECTOR, +} from './storage'; + +export { IdbStorage, LocalStorage, EncryptedIdbStorage } from './storage'; -const KEY_STORAGE_KEY = 'identity'; -const KEY_STORAGE_DELEGATION = 'delegation'; const IDENTITY_PROVIDER_DEFAULT = 'https://identity.ic0.app'; const IDENTITY_PROVIDER_ENDPOINT = '#authorize'; @@ -95,17 +99,6 @@ export interface AuthClientLoginOptions { onError?: ((error?: string) => void) | ((error?: string) => Promise); } -/** - * Interface for persisting user authentication data - */ -export interface AuthClientStorage { - get(key: string): Promise; - - set(key: string, value: string): Promise; - - remove(key: string): Promise; -} - interface InternetIdentityAuthRequest { kind: 'authorize-client'; sessionPublicKey: Uint8Array; @@ -126,123 +119,6 @@ interface InternetIdentityAuthResponseSuccess { userPublicKey: Uint8Array; } -async function _deleteStorage(storage: AuthClientStorage) { - await storage.remove(KEY_STORAGE_KEY); - await storage.remove(KEY_STORAGE_DELEGATION); - await storage.remove(KEY_STORAGE_DELEGATION); -} - -export class LocalStorage implements AuthClientStorage { - constructor(public readonly prefix = 'ic-', private readonly _localStorage?: Storage) {} - - public get(key: string): Promise { - return Promise.resolve(this._getLocalStorage().getItem(this.prefix + key)); - } - - public set(key: string, value: string): Promise { - this._getLocalStorage().setItem(this.prefix + key, value); - return Promise.resolve(); - } - - public remove(key: string): Promise { - this._getLocalStorage().removeItem(this.prefix + key); - return Promise.resolve(); - } - - private _getLocalStorage() { - if (this._localStorage) { - return this._localStorage; - } - - const ls = - typeof window === 'undefined' - ? typeof global === 'undefined' - ? typeof self === 'undefined' - ? undefined - : self.localStorage - : global.localStorage - : window.localStorage; - - if (!ls) { - throw new Error('Could not find local storage.'); - } - - return ls; - } -} - -export class IdbStorage implements AuthClientStorage { - private get encryptKey() { - return new Promise(resolve => { - get('ic-auth-key').then(async storedKey => { - const key = - storedKey ?? - (await crypto.subtle.generateKey( - { - name: 'AES-CBC', - length: 256, - }, - false, - ['encrypt', 'decrypt'], - )); - - await set('ic-auth-key', key); - resolve(key); - }); - }); - } - private get iv() { - return new Promise(resolve => { - get('ic-iv').then(async storedIv => { - const iv = storedIv ?? (await crypto.getRandomValues(new Uint8Array(16))); - - await set('ic-iv', iv); - resolve(iv); - }); - }); - } - - constructor(public readonly prefix = 'ic-') {} - - public async get(key: string): Promise { - const encryptKey = await await this.encryptKey; - const encrypted = await get(this.prefix + key); - if (encrypted) { - const decoder = new TextDecoder(); - - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-CBC', iv: await this.iv }, - encryptKey, - encrypted, - ); - return decoder.decode(decrypted); - } - return null; - } - - public async set(key: string, value: string): Promise { - const encoder = new TextEncoder(); - const encryptKey = await await this.encryptKey; - - try { - await set( - this.prefix + key, - await crypto.subtle.encrypt( - { name: 'AES-CBC', iv: await this.iv }, - encryptKey, - encoder.encode(value).buffer, - ), - ); - } catch (error) { - console.error(error); - } - } - - public async remove(key: string): Promise { - await remove(this.prefix + key); - } -} - interface AuthReadyMessage { kind: 'authorize-ready'; } @@ -311,7 +187,7 @@ export class AuthClient { idleOptions?: IdleOptions; } = {}, ): Promise { - const storage = options.storage ?? new IdbStorage('ic-'); + const storage = options.storage ?? new IdbStorage(); let key: null | SignIdentity = null; if (options.identity) { @@ -608,3 +484,10 @@ export class AuthClient { } } } + +async function _deleteStorage(storage: AuthClientStorage) { + await storage.remove(KEY_STORAGE_KEY); + await storage.remove(KEY_STORAGE_DELEGATION); + await storage.remove(KEY_ENCRYPTION); + await storage.remove(KEY_VECTOR); +} diff --git a/packages/auth-client/src/storage.ts b/packages/auth-client/src/storage.ts new file mode 100644 index 000000000..c1e33af54 --- /dev/null +++ b/packages/auth-client/src/storage.ts @@ -0,0 +1,145 @@ +import { db, getValue, removeValue, setValue } from './db'; +const set = async (key: string, value: T) => await setValue(db, value, key); +const get = async (key: string): Promise => await getValue(db, key); +const remove = async (key: string) => await removeValue(db, key); + +export const KEY_STORAGE_KEY = 'identity'; +export const KEY_STORAGE_DELEGATION = 'delegation'; +export const KEY_ENCRYPTION = 'encrypt-key'; +export const KEY_VECTOR = 'iv'; + +/** + * Interface for persisting user authentication data + */ +export interface AuthClientStorage { + get(key: string): Promise; + + set(key: string, value: string): Promise; + + remove(key: string): Promise; +} + +export class LocalStorage implements AuthClientStorage { + constructor(public readonly prefix = 'ic-', private readonly _localStorage?: Storage) {} + + public get(key: string): Promise { + return Promise.resolve(this._getLocalStorage().getItem(this.prefix + key)); + } + + public set(key: string, value: string): Promise { + this._getLocalStorage().setItem(this.prefix + key, value); + return Promise.resolve(); + } + + public remove(key: string): Promise { + this._getLocalStorage().removeItem(this.prefix + key); + return Promise.resolve(); + } + + private _getLocalStorage() { + if (this._localStorage) { + return this._localStorage; + } + + const ls = + typeof window === 'undefined' + ? typeof global === 'undefined' + ? typeof self === 'undefined' + ? undefined + : self.localStorage + : global.localStorage + : window.localStorage; + + if (!ls) { + throw new Error('Could not find local storage.'); + } + + return ls; + } +} + +export class IdbStorage implements AuthClientStorage { + public async get(key: string): Promise { + return (await get(key)) ?? null; + } + + public async set(key: string, value: string): Promise { + await set(key, value); + } + + public async remove(key: string): Promise { + await remove(key); + } +} +export class EncryptedIdbStorage implements AuthClientStorage { + storedKey: CryptoKey | undefined; + private get encryptKey() { + return new Promise(resolve => { + if (this.storedKey) resolve(this.storedKey); + get(KEY_ENCRYPTION).then(async storedKey => { + const key = + storedKey ?? + (await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + false, + ['encrypt', 'decrypt'], + )); + this.storedKey = storedKey; + + await set(KEY_ENCRYPTION, key); + resolve(key); + }); + }); + } + private get iv() { + return new Promise(resolve => { + get(KEY_VECTOR).then(async storedIv => { + const iv = storedIv ?? (await crypto.getRandomValues(new Uint8Array(16))); + + await set(KEY_VECTOR, iv); + resolve(iv); + }); + }); + } + + public async get(key: string): Promise { + const encryptKey = await await this.encryptKey; + const encrypted = await get(key); + + if (encrypted) { + const decoder = new TextDecoder(); + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-CBC', iv: await this.iv }, + encryptKey, + encrypted, + ); + return decoder.decode(decrypted); + } + return null; + } + + public async set(key: string, value: string): Promise { + const encoder = new TextEncoder(); + const encryptKey = await await this.encryptKey; + + try { + await set( + key, + await crypto.subtle.encrypt( + { name: 'AES-CBC', iv: await this.iv }, + encryptKey, + encoder.encode(value).buffer, + ), + ); + } catch (error) { + console.error(error); + } + } + + public async remove(key: string): Promise { + await remove(key); + } +} diff --git a/packages/auth-client/test-setup.ts b/packages/auth-client/test-setup.ts index 8562441ac..0ed7724aa 100644 --- a/packages/auth-client/test-setup.ts +++ b/packages/auth-client/test-setup.ts @@ -8,7 +8,6 @@ // Node.js in a similar way. import { Crypto } from '@peculiar/webcrypto'; global.crypto = new Crypto(); -import 'fake-indexeddb/auto'; global.TextEncoder = require('text-encoding').TextEncoder; global.TextDecoder = require('text-encoding').TextDecoder; require('whatwg-fetch'); From ca81bbc64e5982b47347ff67d15dda060f8a6038 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Thu, 4 Aug 2022 11:57:21 -0700 Subject: [PATCH 4/6] refactor to general-purpose IdbKeyVal store --- packages/auth-client/src/db.test.ts | 31 ++++++++++--- packages/auth-client/src/db.ts | 69 ++++++++++++++++++++++------- packages/auth-client/src/storage.ts | 69 ++++++++++++++++++++--------- packages/auth-client/test-setup.ts | 1 + 4 files changed, 128 insertions(+), 42 deletions(-) diff --git a/packages/auth-client/src/db.test.ts b/packages/auth-client/src/db.test.ts index b8eee3a38..61bd782ed 100644 --- a/packages/auth-client/src/db.test.ts +++ b/packages/auth-client/src/db.test.ts @@ -1,18 +1,35 @@ -import { db, getValue, removeValue, setValue } from './db'; +import 'fake-indexeddb/auto'; +import { IdbKeyVal } from './db'; + +let testCounter = 0; + +const testDb = async () => { + return await IdbKeyVal.create({ + dbName: 'db-' + testCounter, + storeName: 'store-' + testCounter, + }); +}; + +beforeEach(() => { + testCounter += 1; +}); describe('indexeddb wrapper', () => { it('should store a basic key value', async () => { - const shouldSet = async () => await setValue(db, 'testValue', 'testKey'); + const db = await testDb(); + const shouldSet = async () => await db.set('testKey', 'testValue'); expect(shouldSet).not.toThrow(); - expect(await getValue(db, 'testKey')).toBe('testValue'); + expect(await db.get('testKey')).toBe('testValue'); }); it('should support removing a value', async () => { - await setValue(db, 'testValue', 'testKey'); - expect(await getValue(db, 'testKey')).toBe('testValue'); + const db = await testDb(); + await db.set('testKey', 'testValue'); + + expect(await db.get('testKey')).toBe('testValue'); - await removeValue(db, 'testKey'); + await db.remove('testKey'); - expect(await getValue(db, 'testKey')).toBe(undefined); + expect(await db.get('testKey')).toBe(null); }); }); diff --git a/packages/auth-client/src/db.ts b/packages/auth-client/src/db.ts index d8a69cbff..d6a914dd9 100644 --- a/packages/auth-client/src/db.ts +++ b/packages/auth-client/src/db.ts @@ -1,17 +1,20 @@ import { openDB, IDBPDatabase } from 'idb'; -export type Database = Promise>; -export const AUTH_DB_NAME = 'auth-client-db'; -export const OBJECT_STORE_NAME = 'keyval-store'; +type Database = IDBPDatabase; +const AUTH_DB_NAME = 'auth-client-db'; +const OBJECT_STORE_NAME = 'ic-keyval'; -export const openDbStore = async () => - await openDB(AUTH_DB_NAME, 1, { +// Increment if schema changes +const AUTH_CLIENT_DB_VERSION = 1; + +const openDbStore = async (dbName = AUTH_DB_NAME, storeName = OBJECT_STORE_NAME, version: number) => + await openDB(dbName, version, { upgrade: database => { database.objectStoreNames; - if (database.objectStoreNames.contains(OBJECT_STORE_NAME)) { - database.clear(OBJECT_STORE_NAME); + if (database.objectStoreNames.contains(storeName)) { + database.clear(storeName); } - database.createObjectStore(OBJECT_STORE_NAME); + database.createObjectStore(storeName); }, }); @@ -21,8 +24,12 @@ export const openDbStore = async () => * @param key string * @returns */ -export async function getValue(db: Database, key: string): Promise { - return (await db).get(OBJECT_STORE_NAME, key); +async function getValue( + db: Database, + storeName: string, + key: IDBValidKey, +): Promise { + return await db.get(storeName, key); } /** @@ -31,8 +38,13 @@ export async function getValue(db: Database, key: string): Promise(db: Database, value: T, key: string): Promise { - (await db).put(OBJECT_STORE_NAME, value, key); +async function setValue( + db: Database, + storeName: string, + key: IDBValidKey, + value: T, +): Promise { + return await db.put(storeName, value, key); } /** @@ -41,8 +53,35 @@ export async function setValue(db: Database, value: T, key: string): Promise< * @param value any value to remove * @param key string asdf */ -export async function removeValue(db: Database, key: string): Promise { - (await db).delete(OBJECT_STORE_NAME, key); +async function removeValue(db: Database, storeName: string, key: IDBValidKey): Promise { + return await db.delete(storeName, key); } -export const db = openDbStore(); +export type DBCreateOptions = { + dbName?: string; + storeName?: string; + version?: number; +}; +export class IdbKeyVal { + public static async create(options?: DBCreateOptions) { + const { + dbName = AUTH_DB_NAME, + storeName = OBJECT_STORE_NAME, + version = AUTH_CLIENT_DB_VERSION, + } = options ?? {}; + const db = await openDbStore(dbName, storeName, version); + return new IdbKeyVal(db, storeName); + } + + private constructor(private _db: Database, private _storeName: string) {} + + public async set(key: IDBValidKey, value: T) { + return await setValue(this._db, this._storeName, key, value); + } + public async get(key: IDBValidKey): Promise { + return (await getValue(this._db, this._storeName, key)) ?? null; + } + public async remove(key: IDBValidKey) { + return await removeValue(this._db, this._storeName, key); + } +} diff --git a/packages/auth-client/src/storage.ts b/packages/auth-client/src/storage.ts index c1e33af54..d1ab54000 100644 --- a/packages/auth-client/src/storage.ts +++ b/packages/auth-client/src/storage.ts @@ -1,7 +1,4 @@ -import { db, getValue, removeValue, setValue } from './db'; -const set = async (key: string, value: T) => await setValue(db, value, key); -const get = async (key: string): Promise => await getValue(db, key); -const remove = async (key: string) => await removeValue(db, key); +import { IdbKeyVal, DBCreateOptions } from './db'; export const KEY_STORAGE_KEY = 'identity'; export const KEY_STORAGE_DELEGATION = 'delegation'; @@ -59,26 +56,52 @@ export class LocalStorage implements AuthClientStorage { } export class IdbStorage implements AuthClientStorage { + private initializedDb: IdbKeyVal | undefined; + get _db(): Promise { + return new Promise(resolve => { + if (this.initializedDb) resolve(this.initializedDb); + IdbKeyVal.create().then(db => { + this.initializedDb = db; + resolve(db); + }); + }); + } + public async get(key: string): Promise { - return (await get(key)) ?? null; + const db = await this._db; + return await db.get(key); + // return (await db.get(key)) ?? null; } public async set(key: string, value: string): Promise { - await set(key, value); + const db = await this._db; + await db.set(key, value); } public async remove(key: string): Promise { - await remove(key); + const db = await this._db; + await db.remove(key); } } export class EncryptedIdbStorage implements AuthClientStorage { - storedKey: CryptoKey | undefined; + private initializedDb: IdbKeyVal | undefined; + get _db(): Promise { + return new Promise(resolve => { + if (this.initializedDb) resolve(this.initializedDb); + IdbKeyVal.create().then(db => { + this.initializedDb = db; + resolve(db); + }); + }); + } + + private storedKey: CryptoKey | undefined; private get encryptKey() { return new Promise(resolve => { - if (this.storedKey) resolve(this.storedKey); - get(KEY_ENCRYPTION).then(async storedKey => { + this._db.then(async db => { + if (this.storedKey) resolve(this.storedKey); const key = - storedKey ?? + (await db.get(KEY_ENCRYPTION)) ?? (await crypto.subtle.generateKey( { name: 'AES-CBC', @@ -87,27 +110,31 @@ export class EncryptedIdbStorage implements AuthClientStorage { false, ['encrypt', 'decrypt'], )); - this.storedKey = storedKey; - await set(KEY_ENCRYPTION, key); + this.storedKey = key; + + await db.set(KEY_ENCRYPTION, key); resolve(key); }); }); } private get iv() { return new Promise(resolve => { - get(KEY_VECTOR).then(async storedIv => { - const iv = storedIv ?? (await crypto.getRandomValues(new Uint8Array(16))); + this._db.then(db => { + db.get(KEY_VECTOR).then(async storedIv => { + const iv = storedIv ?? (await crypto.getRandomValues(new Uint8Array(16))); - await set(KEY_VECTOR, iv); - resolve(iv); + await db.set(KEY_VECTOR, iv); + resolve(iv); + }); }); }); } public async get(key: string): Promise { + const db = await this._db; const encryptKey = await await this.encryptKey; - const encrypted = await get(key); + const encrypted = await db.get(key); if (encrypted) { const decoder = new TextDecoder(); @@ -122,11 +149,12 @@ export class EncryptedIdbStorage implements AuthClientStorage { } public async set(key: string, value: string): Promise { + const db = await this._db; const encoder = new TextEncoder(); const encryptKey = await await this.encryptKey; try { - await set( + await db.set( key, await crypto.subtle.encrypt( { name: 'AES-CBC', iv: await this.iv }, @@ -140,6 +168,7 @@ export class EncryptedIdbStorage implements AuthClientStorage { } public async remove(key: string): Promise { - await remove(key); + const db = await this._db; + await db.remove(key); } } diff --git a/packages/auth-client/test-setup.ts b/packages/auth-client/test-setup.ts index 0ed7724aa..497f24ca5 100644 --- a/packages/auth-client/test-setup.ts +++ b/packages/auth-client/test-setup.ts @@ -7,6 +7,7 @@ // Note that we can use webpack configuration to make some features available to // Node.js in a similar way. import { Crypto } from '@peculiar/webcrypto'; +import 'fake-indexeddb/auto'; global.crypto = new Crypto(); global.TextEncoder = require('text-encoding').TextEncoder; global.TextDecoder = require('text-encoding').TextDecoder; From c15cb5c4d347d3f239a5748915709ead9d8ff732 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Thu, 4 Aug 2022 12:32:06 -0700 Subject: [PATCH 5/6] removes EncryptedIdbStorage --- packages/auth-client/src/db.ts | 9 +-- packages/auth-client/src/index.test.ts | 8 +- packages/auth-client/src/index.ts | 7 +- packages/auth-client/src/storage.ts | 103 +++---------------------- 4 files changed, 16 insertions(+), 111 deletions(-) diff --git a/packages/auth-client/src/db.ts b/packages/auth-client/src/db.ts index d6a914dd9..d186966c7 100644 --- a/packages/auth-client/src/db.ts +++ b/packages/auth-client/src/db.ts @@ -4,9 +4,6 @@ type Database = IDBPDatabase; const AUTH_DB_NAME = 'auth-client-db'; const OBJECT_STORE_NAME = 'ic-keyval'; -// Increment if schema changes -const AUTH_CLIENT_DB_VERSION = 1; - const openDbStore = async (dbName = AUTH_DB_NAME, storeName = OBJECT_STORE_NAME, version: number) => await openDB(dbName, version, { upgrade: database => { @@ -64,11 +61,7 @@ export type DBCreateOptions = { }; export class IdbKeyVal { public static async create(options?: DBCreateOptions) { - const { - dbName = AUTH_DB_NAME, - storeName = OBJECT_STORE_NAME, - version = AUTH_CLIENT_DB_VERSION, - } = options ?? {}; + const { dbName = AUTH_DB_NAME, storeName = OBJECT_STORE_NAME, version = 1 } = options ?? {}; const db = await openDbStore(dbName, storeName, version); return new IdbKeyVal(db, storeName); } diff --git a/packages/auth-client/src/index.test.ts b/packages/auth-client/src/index.test.ts index 46a3bdd19..c1ec25475 100644 --- a/packages/auth-client/src/index.test.ts +++ b/packages/auth-client/src/index.test.ts @@ -5,6 +5,7 @@ import { IDL } from '@dfinity/candid'; import { Ed25519KeyIdentity } from '@dfinity/identity'; import { Principal } from '@dfinity/principal'; import { AuthClient, ERROR_USER_INTERRUPT, IdbStorage } from './index'; +import { AuthClientStorage } from './storage'; /** * A class for mocking the IDP service. @@ -76,7 +77,6 @@ describe('Auth Client', () => { fetch, toString: jest.fn(() => 'http://localhost:8000'), }; - const newLocation = window.location; const identity = Ed25519KeyIdentity.generate(); const mockFetch: jest.Mock = jest.fn(); @@ -331,12 +331,6 @@ describe('IdbStorage', () => { }); }); -describe('EncryptedIdbStorage', () => { - it.skip('should handle get and set', async () => { - // The CryptoKey does not currently interact correctly with the fake-indexeddb mock - }); -}); - // A minimal interface of our interactions with the Window object of the IDP. interface IdpWindow { postMessage(message: { kind: string }): void; diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts index 5699189cc..fcf7b1c35 100644 --- a/packages/auth-client/src/index.ts +++ b/packages/auth-client/src/index.ts @@ -18,13 +18,13 @@ import { IdleManager, IdleManagerOptions } from './idleManager'; import { AuthClientStorage, IdbStorage, - KEY_ENCRYPTION, KEY_STORAGE_DELEGATION, KEY_STORAGE_KEY, KEY_VECTOR, } from './storage'; -export { IdbStorage, LocalStorage, EncryptedIdbStorage } from './storage'; +export { IdbStorage, LocalStorage } from './storage'; +export { IdbKeyVal, DBCreateOptions } from './db'; const IDENTITY_PROVIDER_DEFAULT = 'https://identity.ic0.app'; const IDENTITY_PROVIDER_ENDPOINT = '#authorize'; @@ -42,7 +42,7 @@ export interface AuthClientCreateOptions { */ identity?: SignIdentity; /** - * Optional storage with get, set, and remove. Uses LocalStorage by default + * Optional storage with get, set, and remove. Uses {@link IdbStorage} by default */ storage?: AuthClientStorage; /** @@ -488,6 +488,5 @@ export class AuthClient { async function _deleteStorage(storage: AuthClientStorage) { await storage.remove(KEY_STORAGE_KEY); await storage.remove(KEY_STORAGE_DELEGATION); - await storage.remove(KEY_ENCRYPTION); await storage.remove(KEY_VECTOR); } diff --git a/packages/auth-client/src/storage.ts b/packages/auth-client/src/storage.ts index d1ab54000..75166c514 100644 --- a/packages/auth-client/src/storage.ts +++ b/packages/auth-client/src/storage.ts @@ -1,9 +1,10 @@ -import { IdbKeyVal, DBCreateOptions } from './db'; +import { IdbKeyVal } from './db'; export const KEY_STORAGE_KEY = 'identity'; export const KEY_STORAGE_DELEGATION = 'delegation'; -export const KEY_ENCRYPTION = 'encrypt-key'; export const KEY_VECTOR = 'iv'; +// Increment if any fields are modified +export const DB_VERSION = 1; /** * Interface for persisting user authentication data @@ -55,12 +56,19 @@ export class LocalStorage implements AuthClientStorage { } } +/** + * IdbStorage is an interface for simple storage of string key-value pairs built on {@link IdbKeyVal} + * + * It replaces {@link LocalStorage} + * @see implements {@link AuthClientStorage} + */ export class IdbStorage implements AuthClientStorage { + // Intializes a KeyVal on first request private initializedDb: IdbKeyVal | undefined; get _db(): Promise { return new Promise(resolve => { if (this.initializedDb) resolve(this.initializedDb); - IdbKeyVal.create().then(db => { + IdbKeyVal.create({ version: DB_VERSION }).then(db => { this.initializedDb = db; resolve(db); }); @@ -83,92 +91,3 @@ export class IdbStorage implements AuthClientStorage { await db.remove(key); } } -export class EncryptedIdbStorage implements AuthClientStorage { - private initializedDb: IdbKeyVal | undefined; - get _db(): Promise { - return new Promise(resolve => { - if (this.initializedDb) resolve(this.initializedDb); - IdbKeyVal.create().then(db => { - this.initializedDb = db; - resolve(db); - }); - }); - } - - private storedKey: CryptoKey | undefined; - private get encryptKey() { - return new Promise(resolve => { - this._db.then(async db => { - if (this.storedKey) resolve(this.storedKey); - const key = - (await db.get(KEY_ENCRYPTION)) ?? - (await crypto.subtle.generateKey( - { - name: 'AES-CBC', - length: 256, - }, - false, - ['encrypt', 'decrypt'], - )); - - this.storedKey = key; - - await db.set(KEY_ENCRYPTION, key); - resolve(key); - }); - }); - } - private get iv() { - return new Promise(resolve => { - this._db.then(db => { - db.get(KEY_VECTOR).then(async storedIv => { - const iv = storedIv ?? (await crypto.getRandomValues(new Uint8Array(16))); - - await db.set(KEY_VECTOR, iv); - resolve(iv); - }); - }); - }); - } - - public async get(key: string): Promise { - const db = await this._db; - const encryptKey = await await this.encryptKey; - const encrypted = await db.get(key); - - if (encrypted) { - const decoder = new TextDecoder(); - const decrypted = await crypto.subtle.decrypt( - { name: 'AES-CBC', iv: await this.iv }, - encryptKey, - encrypted, - ); - return decoder.decode(decrypted); - } - return null; - } - - public async set(key: string, value: string): Promise { - const db = await this._db; - const encoder = new TextEncoder(); - const encryptKey = await await this.encryptKey; - - try { - await db.set( - key, - await crypto.subtle.encrypt( - { name: 'AES-CBC', iv: await this.iv }, - encryptKey, - encoder.encode(value).buffer, - ), - ); - } catch (error) { - console.error(error); - } - } - - public async remove(key: string): Promise { - const db = await this._db; - await db.remove(key); - } -} From 60af6588b1f0e71adb658c620886d7a43c6b1bf7 Mon Sep 17 00:00:00 2001 From: Kyle Peacock Date: Thu, 4 Aug 2022 13:56:44 -0700 Subject: [PATCH 6/6] documentation --- docs/generated/changelog.html | 10 ++++ packages/auth-client/src/db.ts | 86 +++++++++++++++++++---------- packages/auth-client/src/storage.ts | 3 + 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html index 1b980f8bf..4e86bf006 100644 --- a/docs/generated/changelog.html +++ b/docs/generated/changelog.html @@ -10,6 +10,16 @@

Agent-JS Changelog

+

Version 0.12.3

+
    +
  • + AuthClient now uses IndexedDb by default. To use localStorage, import LocalStorage + provider and pass it during AuthClient.create(). +
  • +
      +
    • Also offers a generic Indexed Db keyval store, IdbKeyVal
    • +
    +

Version 0.12.2

  • diff --git a/packages/auth-client/src/db.ts b/packages/auth-client/src/db.ts index d186966c7..adc3f8f73 100644 --- a/packages/auth-client/src/db.ts +++ b/packages/auth-client/src/db.ts @@ -1,11 +1,22 @@ import { openDB, IDBPDatabase } from 'idb'; +import { KEY_STORAGE_DELEGATION, KEY_STORAGE_KEY } from './storage'; type Database = IDBPDatabase; +type IDBValidKey = string | number | Date | BufferSource | IDBValidKey[]; const AUTH_DB_NAME = 'auth-client-db'; const OBJECT_STORE_NAME = 'ic-keyval'; -const openDbStore = async (dbName = AUTH_DB_NAME, storeName = OBJECT_STORE_NAME, version: number) => - await openDB(dbName, version, { +const _openDbStore = async ( + dbName = AUTH_DB_NAME, + storeName = OBJECT_STORE_NAME, + version: number, +) => { + // Clear legacy stored delegations + if (localStorage && localStorage.getItem(KEY_STORAGE_DELEGATION)) { + localStorage.removeItem(KEY_STORAGE_DELEGATION); + localStorage.removeItem(KEY_STORAGE_KEY); + } + return await openDB(dbName, version, { upgrade: database => { database.objectStoreNames; if (database.objectStoreNames.contains(storeName)) { @@ -14,14 +25,9 @@ const openDbStore = async (dbName = AUTH_DB_NAME, storeName = OBJECT_STORE_NAME, database.createObjectStore(storeName); }, }); +}; -/** - * - * @param db database - * @param key string - * @returns - */ -async function getValue( +async function _getValue( db: Database, storeName: string, key: IDBValidKey, @@ -29,13 +35,7 @@ async function getValue( return await db.get(storeName, key); } -/** - * - * @param db database - * @param value any value to set - * @param key string asdf - */ -async function setValue( +async function _setValue( db: Database, storeName: string, key: IDBValidKey, @@ -44,13 +44,7 @@ async function setValue( return await db.put(storeName, value, key); } -/** - * - * @param db database - * @param value any value to remove - * @param key string asdf - */ -async function removeValue(db: Database, storeName: string, key: IDBValidKey): Promise { +async function _removeValue(db: Database, storeName: string, key: IDBValidKey): Promise { return await db.delete(storeName, key); } @@ -59,22 +53,58 @@ export type DBCreateOptions = { storeName?: string; version?: number; }; + +/** + * Simple Key Value store + * Defaults to `'auth-client-db'` with an object store of `'ic-keyval'` + */ export class IdbKeyVal { - public static async create(options?: DBCreateOptions) { + /** + * + * @param {DBCreateOptions} options {@link DbCreateOptions} + * @param {DBCreateOptions['dbName']} options.dbName name for the indexeddb database + * @default 'auth-client-db' + * @param {DBCreateOptions['storeName']} options.storeName name for the indexeddb Data Store + * @default 'ic-keyval' + * @param {DBCreateOptions['version']} options.version version of the database. Increment to safely upgrade + * @constructs an {@link IdbKeyVal} + */ + public static async create(options?: DBCreateOptions): Promise { const { dbName = AUTH_DB_NAME, storeName = OBJECT_STORE_NAME, version = 1 } = options ?? {}; - const db = await openDbStore(dbName, storeName, version); + const db = await _openDbStore(dbName, storeName, version); return new IdbKeyVal(db, storeName); } + // Do not use - instead prefer create private constructor(private _db: Database, private _storeName: string) {} + /** + * Basic setter + * @param {IDBValidKey} key string | number | Date | BufferSource | IDBValidKey[] + * @param value value to set + * @returns void + */ public async set(key: IDBValidKey, value: T) { - return await setValue(this._db, this._storeName, key, value); + return await _setValue(this._db, this._storeName, key, value); } + /** + * Basic getter + * Pass in a type T for type safety if you know the type the value will have if it is found + * @param {IDBValidKey} key string | number | Date | BufferSource | IDBValidKey[] + * @returns `Promise` + * @example + * await get('exampleKey') -> 'exampleValue' + */ public async get(key: IDBValidKey): Promise { - return (await getValue(this._db, this._storeName, key)) ?? null; + return (await _getValue(this._db, this._storeName, key)) ?? null; } + + /** + * Remove a key + * @param key {@link IDBValidKey} + * @returns void + */ public async remove(key: IDBValidKey) { - return await removeValue(this._db, this._storeName, key); + return await _removeValue(this._db, this._storeName, key); } } diff --git a/packages/auth-client/src/storage.ts b/packages/auth-client/src/storage.ts index 75166c514..2a43876a3 100644 --- a/packages/auth-client/src/storage.ts +++ b/packages/auth-client/src/storage.ts @@ -17,6 +17,9 @@ export interface AuthClientStorage { remove(key: string): Promise; } +/** + * Legacy implementation of AuthClientStorage, for use where IndexedDb is not available + */ export class LocalStorage implements AuthClientStorage { constructor(public readonly prefix = 'ic-', private readonly _localStorage?: Storage) {}