From dd4f88987bce3f1bb44d0649f2bf590500ebf698 Mon Sep 17 00:00:00 2001 From: Oliver Dunk Date: Thu, 17 Mar 2022 00:15:46 +0000 Subject: [PATCH 1/8] Add polyfill for secure storage proposal --- proposals/secure-storage-polyfill.js | 67 ++++++++++++++++++++++++++++ proposals/secure-storage.md | 2 + 2 files changed, 69 insertions(+) create mode 100644 proposals/secure-storage-polyfill.js diff --git a/proposals/secure-storage-polyfill.js b/proposals/secure-storage-polyfill.js new file mode 100644 index 00000000..4201238f --- /dev/null +++ b/proposals/secure-storage-polyfill.js @@ -0,0 +1,67 @@ +// Existing browser APIs don't give us access to the system's secure storage, +// meaning all data in this polyfill is stored in less secure mechanisms. +console.warn( + "Warning: browser.secureStorage polyfill loaded. This proof of concept stores data insecurely and should not be used in production." +); + +const RECOGNISED_AUTH_METHODS = [ + "PIN", + "PASSWORD", + "BIOMETRY_FACE", + "BIOMETRY_FINGERPRINT", +]; + +window.secureStorage = { + getInfo: () => { + return { + type: "MACOS_KEYCHAIN", + availableAuthentication: RECOGNISED_AUTH_METHODS, + }; + }, + store: (request) => { + if (typeof request !== "object") + throw new Error("secureStorage.store takes an object"); + + const { id, authentication, data } = request; + + if (typeof id !== "string") throw new Error("id must be a string"); + + if (typeof authentication !== "undefined") { + if (!Array.isArray(authentication)) + throw new Error("authentication must be an array"); + + if (authentication.length === 0) + throw new Error("authentication array must be non-empty if present"); + + for (const method of authentication) { + if (!RECOGNISED_AUTH_METHODS.includes(method)) { + throw new Error(`unrecognised auth method: ${method}`); + } + } + } + + if (typeof data !== "string") throw new Error("data must be a string"); + + localStorage.setItem(id, data); + }, + retrieve: (request) => { + if (typeof request !== "object") + throw new Error("secureStorage.retrieve takes an object"); + + const { id } = request; + + if (typeof id !== "string") throw new Error("id must be a string"); + + return localStorage.getItem(id); + }, + remove: (request) => { + if (typeof request !== "object") + throw new Error("secureStorage.remove takes an object"); + + const { id } = request; + + if (typeof id !== "string") throw new Error("id must be a string"); + + return localStorage.removeItem(id); + }, +}; diff --git a/proposals/secure-storage.md b/proposals/secure-storage.md index 91e2ab11..40ac95f9 100644 --- a/proposals/secure-storage.md +++ b/proposals/secure-storage.md @@ -100,6 +100,8 @@ browser.secureStorage.store({ }); ``` +The authentication array is optional. If omitted, the secret is available without biometrics but is still stored in the hardware backed location. + **browser.secureStorage.retrieve** This retrieves the stored data. The browser will only provide it if the user authenticates with one of the allowed mechanisms for this secret, and will throw an error otherwise. From 5d2adc0fce6835ffe565cc1debb629631f08be79 Mon Sep 17 00:00:00 2001 From: Oliver Dunk Date: Thu, 17 Mar 2022 00:17:31 +0000 Subject: [PATCH 2/8] Reference polyfill in secure storage proposal --- proposals/secure-storage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/secure-storage.md b/proposals/secure-storage.md index 40ac95f9..1875e3c5 100644 --- a/proposals/secure-storage.md +++ b/proposals/secure-storage.md @@ -25,7 +25,7 @@ While all of these APIs support storing keys, only macOS Keychain provides a mec - **Proposal 1:** The browser provides a way of storing keys exclusively, and simply retrieves these from the system level APIs. -- **Proposal 2:** The browser accepts arbitrary data from an extension. Where possible, this is simply stored using the system level APIs. When the system only provides key storage, the browser transparently generates a key and uses this to encrypt the data. +- **Proposal 2:** The browser accepts arbitrary data from an extension. Where possible, this is simply stored using the system level APIs. When the system only provides key storage, the browser transparently generates a key and uses this to encrypt the data. A polyfill for this proposal is available [here](secure-storage-polyfill.js). In both cases data can only be read by the extension that stored it. From c39adc242ca2011556015b5c81ae3af9beabce0a Mon Sep 17 00:00:00 2001 From: Oliver Dunk Date: Sun, 20 Mar 2022 13:46:34 +0000 Subject: [PATCH 3/8] Attach secureStorage to browser/chrome namespaces --- proposals/secure-storage-polyfill.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/proposals/secure-storage-polyfill.js b/proposals/secure-storage-polyfill.js index 4201238f..1e1eaf07 100644 --- a/proposals/secure-storage-polyfill.js +++ b/proposals/secure-storage-polyfill.js @@ -11,7 +11,7 @@ const RECOGNISED_AUTH_METHODS = [ "BIOMETRY_FINGERPRINT", ]; -window.secureStorage = { +const secureStorage = { getInfo: () => { return { type: "MACOS_KEYCHAIN", @@ -65,3 +65,19 @@ window.secureStorage = { return localStorage.removeItem(id); }, }; + +let isExtension = false; + +if (window.browser?.runtime?.id) { + window.browser.secureStorage = secureStorage; + isExtension = true; +} + +if (window.chrome?.runtime?.id) { + window.chrome.secureStorage = secureStorage; + isExtension = true; +} + +if (!isExtension) { + window.secureStorage = secureStorage; +} From 05e9757009d62d8d87127f732ec7d5c81b08807a Mon Sep 17 00:00:00 2001 From: Oliver Dunk Date: Sun, 20 Mar 2022 13:48:07 +0000 Subject: [PATCH 4/8] Make secureStorage polyfill promise based --- proposals/secure-storage-polyfill.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/secure-storage-polyfill.js b/proposals/secure-storage-polyfill.js index 1e1eaf07..13dcc8af 100644 --- a/proposals/secure-storage-polyfill.js +++ b/proposals/secure-storage-polyfill.js @@ -12,13 +12,13 @@ const RECOGNISED_AUTH_METHODS = [ ]; const secureStorage = { - getInfo: () => { + getInfo: async () => { return { type: "MACOS_KEYCHAIN", availableAuthentication: RECOGNISED_AUTH_METHODS, }; }, - store: (request) => { + store: async (request) => { if (typeof request !== "object") throw new Error("secureStorage.store takes an object"); @@ -44,7 +44,7 @@ const secureStorage = { localStorage.setItem(id, data); }, - retrieve: (request) => { + retrieve: async (request) => { if (typeof request !== "object") throw new Error("secureStorage.retrieve takes an object"); @@ -54,7 +54,7 @@ const secureStorage = { return localStorage.getItem(id); }, - remove: (request) => { + remove: async (request) => { if (typeof request !== "object") throw new Error("secureStorage.remove takes an object"); From 1d6f41a2edb1a8c7dd2c65955a1265bdea34d50c Mon Sep 17 00:00:00 2001 From: Oliver Dunk Date: Sun, 20 Mar 2022 13:49:14 +0000 Subject: [PATCH 5/8] Clarify optional authentication array --- proposals/secure-storage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/secure-storage.md b/proposals/secure-storage.md index 1875e3c5..bf6566aa 100644 --- a/proposals/secure-storage.md +++ b/proposals/secure-storage.md @@ -100,7 +100,7 @@ browser.secureStorage.store({ }); ``` -The authentication array is optional. If omitted, the secret is available without biometrics but is still stored in the hardware backed location. +The authentication array is optional. If omitted, the secret is available without the need for any of the recognised auth methods but is still stored in the hardware backed location. **browser.secureStorage.retrieve** From 3cc45cad1f950e9507db5c5443ec47245aea6c51 Mon Sep 17 00:00:00 2001 From: Oliver Dunk Date: Sun, 20 Mar 2022 14:30:54 +0000 Subject: [PATCH 6/8] Use browser.storage.local API instead of localStorage --- proposals/secure-storage-polyfill.js | 36 +++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/proposals/secure-storage-polyfill.js b/proposals/secure-storage-polyfill.js index 13dcc8af..aa7c3c26 100644 --- a/proposals/secure-storage-polyfill.js +++ b/proposals/secure-storage-polyfill.js @@ -1,3 +1,19 @@ +let global; + +if (window.browser?.runtime.id) { + global = window.browser; +} else if (window.chrome?.runtime.id) { + global = window.chrome; +} else { + throw new Error( + "browser.secureStorage polyfill must be run in extension contexts" + ); +} + +if (!global.storage?.local) { + throw new Error("Using this polyfill requires the 'storage' permission"); +} + // Existing browser APIs don't give us access to the system's secure storage, // meaning all data in this polyfill is stored in less secure mechanisms. console.warn( @@ -42,7 +58,9 @@ const secureStorage = { if (typeof data !== "string") throw new Error("data must be a string"); - localStorage.setItem(id, data); + return new Promise((resolve) => + global.storage.local.set({ [id]: data }, resolve) + ); }, retrieve: async (request) => { if (typeof request !== "object") @@ -52,7 +70,9 @@ const secureStorage = { if (typeof id !== "string") throw new Error("id must be a string"); - return localStorage.getItem(id); + return new Promise((resolve) => { + global.storage.local.get(id, (result) => resolve(result[id])); + }); }, remove: async (request) => { if (typeof request !== "object") @@ -62,22 +82,16 @@ const secureStorage = { if (typeof id !== "string") throw new Error("id must be a string"); - return localStorage.removeItem(id); + return new Promise((resolve) => global.storage.local.remove(id, resolve)); }, }; -let isExtension = false; - +// Attach to browser namespace in Firefox/Safari if (window.browser?.runtime?.id) { window.browser.secureStorage = secureStorage; - isExtension = true; } +// Attach to chrome namespace in all browsers if (window.chrome?.runtime?.id) { window.chrome.secureStorage = secureStorage; - isExtension = true; -} - -if (!isExtension) { - window.secureStorage = secureStorage; } From bc94c3dd883569720405a21749059fa659856bab Mon Sep 17 00:00:00 2001 From: Oliver Dunk Date: Sun, 20 Mar 2022 14:40:33 +0000 Subject: [PATCH 7/8] Use globalThis to support service worker environments --- proposals/secure-storage-polyfill.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/proposals/secure-storage-polyfill.js b/proposals/secure-storage-polyfill.js index aa7c3c26..1da1d2d1 100644 --- a/proposals/secure-storage-polyfill.js +++ b/proposals/secure-storage-polyfill.js @@ -1,9 +1,9 @@ let global; -if (window.browser?.runtime.id) { - global = window.browser; -} else if (window.chrome?.runtime.id) { - global = window.chrome; +if (globalThis.browser?.runtime.id) { + global = browser; +} else if (globalThis.chrome?.runtime.id) { + global = chrome; } else { throw new Error( "browser.secureStorage polyfill must be run in extension contexts" @@ -87,11 +87,11 @@ const secureStorage = { }; // Attach to browser namespace in Firefox/Safari -if (window.browser?.runtime?.id) { - window.browser.secureStorage = secureStorage; +if (globalThis.browser?.runtime?.id) { + browser.secureStorage = secureStorage; } // Attach to chrome namespace in all browsers -if (window.chrome?.runtime?.id) { - window.chrome.secureStorage = secureStorage; +if (globalThis.chrome?.runtime?.id) { + chrome.secureStorage = secureStorage; } From d8be27010d9933e240b48472d1c205c300ab8344 Mon Sep 17 00:00:00 2001 From: Oliver Dunk Date: Thu, 9 Jun 2022 00:21:27 +0100 Subject: [PATCH 8/8] Rename "polyfill" to "mock" --- ...{secure-storage-polyfill.js => secure-storage-mock.js} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename proposals/{secure-storage-polyfill.js => secure-storage-mock.js} (87%) diff --git a/proposals/secure-storage-polyfill.js b/proposals/secure-storage-mock.js similarity index 87% rename from proposals/secure-storage-polyfill.js rename to proposals/secure-storage-mock.js index 1da1d2d1..b86f957a 100644 --- a/proposals/secure-storage-polyfill.js +++ b/proposals/secure-storage-mock.js @@ -6,18 +6,18 @@ if (globalThis.browser?.runtime.id) { global = chrome; } else { throw new Error( - "browser.secureStorage polyfill must be run in extension contexts" + "browser.secureStorage mock must be run in extension contexts" ); } if (!global.storage?.local) { - throw new Error("Using this polyfill requires the 'storage' permission"); + throw new Error("Using this mock requires the 'storage' permission"); } // Existing browser APIs don't give us access to the system's secure storage, -// meaning all data in this polyfill is stored in less secure mechanisms. +// meaning all data in this mock is stored in less secure mechanisms. console.warn( - "Warning: browser.secureStorage polyfill loaded. This proof of concept stores data insecurely and should not be used in production." + "Warning: browser.secureStorage mock loaded. This proof of concept stores data insecurely and should not be used in production." ); const RECOGNISED_AUTH_METHODS = [