diff --git a/background/background.js b/background/background.js index 2ead8f04..c057dcc6 100644 --- a/background/background.js +++ b/background/background.js @@ -7,7 +7,7 @@ Settings can call secureCacheMemory, which in turn can open new ports to this script. */ -import { ProtectedMemory } from '$services/protectedMemory.js' +import { ProtectedMemory } from '$services/protectedMemory' import { Settings } from '$services/settings.js' import { Notifications } from "$services/notifications"; diff --git a/scripts/manifest.ts b/scripts/manifest.ts index 15d092e6..2b29bdaa 100644 --- a/scripts/manifest.ts +++ b/scripts/manifest.ts @@ -39,7 +39,7 @@ const permissions = [ const baseManifest: Manifest.WebExtensionManifest = { "name": "KeePass Tusk - Password Access and Autofill", "short_name": "KeePass Tusk", - "version": "2024.8.6", + "version": "2024.8.12", "description": "Readonly KeePass password database integration for Chrome and Firefox", "icons": { "16": "/assets/16x16.png", @@ -75,12 +75,12 @@ const baseManifest: Manifest.WebExtensionManifest = { function chromeManifestV3(): Manifest.WebExtensionManifest { return Object.assign({}, baseManifest, { "manifest_version": 3, - "minimum_chrome_version": "88", + "minimum_chrome_version": "102", "oauth2": { // The Chrome identity client ID for Chrome ONLY "client_id": "876467817034-al13p9m2bphgregs0rij76n1tumakcqr.apps.googleusercontent.com", "scopes": [ - "https://www.googleapis.com/auth/drive.readonly" + "https://www.googleapis.com/auth/drive.file" ] }, permissions, diff --git a/services/protectedMemory.js b/services/protectedMemory.ts similarity index 58% rename from services/protectedMemory.js rename to services/protectedMemory.ts index 570197dc..6d8b1982 100644 --- a/services/protectedMemory.js +++ b/services/protectedMemory.ts @@ -1,12 +1,13 @@ "use strict"; /** - * Storage in RAM, just not in the clear. Purpose is to prevent seeing the + * Storage in storage.session, just not in the clear. Purpose is to prevent seeing the * contents in a casual scan of RAM. Does not prevent an attacker with direct - * access to the code from reading the contents. + * access to the code from reading the contents. Kind of performative, TBH. */ import * as Base64 from 'base64-arraybuffer' +import browser from 'webextension-polyfill' function ProtectedMemory() { var my = { @@ -18,29 +19,40 @@ function ProtectedMemory() { hydrate: deserialize //not encrypted } - var dataMap = {}; + // Static initialization vector. Again, the point is to prevent casual scanning. var AES = { name: "AES-CBC", - iv: crypto.getRandomValues(new Uint8Array(16)) + iv: new Uint8Array([151, 130, 214, 18, 4, 148, 135, 72, 253, 242, 1, 203, 18, 45, 45, 180]) }; - var keyPromise = initNewKey(); - - function initNewKey() { - return crypto.subtle.generateKey({ - name: "AES-CBC", - length: 256 - }, false, ["encrypt", "decrypt"]); + async function getCryptoKey() { + const key = (await browser.storage.session.get("__key__"))["__key__"]; + if (key === undefined) { + const newKey = await crypto.subtle.generateKey({ + name: AES.name, + length: 256 + }, true, ["encrypt", "decrypt"]); + const exported = await crypto.subtle.exportKey("raw", newKey); + const serialized = serialize(exported); + await browser.storage.session.set({ "__key__": serialized }); + console.log("Generated protected memory key"); + return newKey; + } + const deSerialized = deserialize(key); + const cryptoKey = await crypto.subtle.importKey("raw", deSerialized, AES.name, false, ["encrypt", "decrypt"]); + return cryptoKey; } - function getData(key) { - var encData = dataMap[key]; - if (encData === undefined || typeof (encData) !== 'string') + async function getData(key: string) { + var encData = (await browser.storage.session.get(key))[key]; + if (encData === undefined || typeof (encData) !== 'string') { + console.log("Cache miss for " + key); return Promise.resolve(undefined); - - return keyPromise.then(key => { + } + console.log("Cache hit for " + key); + return getCryptoKey().then(cryptoKey => { var encBytes = Base64.decode(encData); - return crypto.subtle.decrypt(AES, key, encBytes); + return crypto.subtle.decrypt(AES, cryptoKey, encBytes); }).then(function (data) { var decoder = new TextDecoder(); var decoded = decoder.decode(new Uint8Array(data)); @@ -49,37 +61,36 @@ function ProtectedMemory() { }); } - function setData(key, data) { + function setData(key: string, data: any) { + console.log("Set cache for " + key); var preppedData = prepData(data); var encoder = new TextEncoder(); var dataBytes = encoder.encode(JSON.stringify(preppedData)); - return keyPromise.then(key => { - return crypto.subtle.encrypt(AES, key, dataBytes); + return getCryptoKey().then(cryptoKey => { + return crypto.subtle.encrypt(AES, cryptoKey, dataBytes); }).then(function (encData) { var dataString = Base64.encode(encData); - dataMap[key] = dataString; - return Promise.resolve(); + return browser.storage.session.set({ [key]: dataString }); }); } - function clearData(key) { + function clearData(key: string) { + console.log("Clear protected memory.") if (key !== undefined) { - delete dataMap[key] + return browser.storage.session.remove(key); } else { - dataMap = {}; - keyPromise = initNewKey(); + return browser.storage.session.clear(); } - return keyPromise } - function serialize(data) { + function serialize(data: any) { var preppedData = prepData(data); var encoder = new TextEncoder(); var dataBytes = encoder.encode(JSON.stringify(preppedData)); return Base64.encode(dataBytes); } - function deserialize(serializedData) { + function deserialize(serializedData: string) { if (serializedData === undefined || typeof (serializedData) !== 'string' || serializedData === "") return undefined; @@ -95,7 +106,7 @@ function ProtectedMemory() { * Also makes a deep copy, so what is returned is not the original. */ var randomString = "Ựៅ" // Base64.encode(crypto.getRandomValues(new Uint8Array(4))); - function prepData(data) { + function prepData(data: any) { if (data === null || data === undefined || typeof (data) !== 'object') return data; @@ -108,7 +119,7 @@ function ProtectedMemory() { } return newArray; } else { - var newObject = {}; + var newObject: Record = {}; for (var prop in data) { newObject[prop] = prepData(data[prop]); } @@ -116,7 +127,7 @@ function ProtectedMemory() { } } - function dePrepData(data) { + function dePrepData(data: any) { if (data === null || data === undefined || (typeof (data) !== 'object' && typeof (data) !== 'string')) return data; diff --git a/src/Options.vue b/src/Options.vue index ac1f24b0..22a0acd0 100644 --- a/src/Options.vue +++ b/src/Options.vue @@ -36,7 +36,7 @@ // Singletons import { ChromePromiseApi } from '@/lib/chrome-api-promise.js' import { Settings } from '$services/settings.js' -import { ProtectedMemory } from '$services/protectedMemory.js' +import { ProtectedMemory } from '$services/protectedMemory' import { SecureCacheMemory } from '$services/secureCacheMemory.js' import { PasswordFileStoreRegistry } from '$services/passwordFileStore.js' import { KeyFileParser } from '$services/keyFileParser.js' diff --git a/src/Popup.vue b/src/Popup.vue index bb9d94d6..fc713529 100644 --- a/src/Popup.vue +++ b/src/Popup.vue @@ -28,7 +28,7 @@ /* beautify preserve:start */ // Singletons import { Settings } from '$services/settings.js' -import { ProtectedMemory } from '$services/protectedMemory.js' +import { ProtectedMemory } from '$services/protectedMemory' import { KeepassHeader } from '$services/keepassHeader.js' import { KeepassReference } from '$services/keepassReference.js' import { KeepassService } from '$services/keepassService.js' diff --git a/src/components/GooglePicker.vue b/src/components/GooglePicker.vue index 8635ff20..62dcc83a 100644 --- a/src/components/GooglePicker.vue +++ b/src/components/GooglePicker.vue @@ -50,7 +50,8 @@ export default {