Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service worker state fixes, Google Drive Permissions fixes #355

Merged
merged 6 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion background/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
6 changes: 3 additions & 3 deletions scripts/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc #354

I caused a regression here by reverting an earlier change to drive.readonly (the wrong permission).

]
},
permissions,
Expand Down
75 changes: 43 additions & 32 deletions services/protectedMemory.js → services/protectedMemory.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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));
Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -108,15 +119,15 @@ function ProtectedMemory() {
}
return newArray;
} else {
var newObject = {};
var newObject: Record<string, unknown> = {};
for (var prop in data) {
newObject[prop] = prepData(data[prop]);
}
return newObject;
}
}

function dePrepData(data) {
function dePrepData(data: any) {
if (data === null || data === undefined || (typeof (data) !== 'object' && typeof (data) !== 'string'))
return data;

Expand Down
2 changes: 1 addition & 1 deletion src/Options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion src/Popup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion src/components/GooglePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export default {
<template>
<div>
<div class="warn pill">
<p><b>Google Drive support has updated!</b> You can now grant Tusk access to each keepass file.</p>
<p><b>Google Drive support has updated!</b> You can now grant Tusk access to each keepass file.
<br>Having problems? <b><a href="https://github.com/subdavis/Tusk/wiki/Troubleshooting#google-drive-issues">Read the troubleshooting guide.</a></b></p>
</div>
<div
v-show="!pickerOpen"
Expand Down
4 changes: 3 additions & 1 deletion src/components/ManageDatabases.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export default {
<div>
<div class="box-bar about roomy">
<p>Tusk
<b>requires</b> that you enable at least one of these cloud storage providers to sync your keepass database with. Once the files appear below, they will be available to unlock within the popup window.</p>
<b>requires</b> that you enable at least one of these cloud storage providers to sync your keepass database with. Once the files appear below, they will be available to unlock within the popup window.
If you have problems, please read <a href="https://github.com/subdavis/Tusk/wiki/Troubleshooting#google-drive-issues">the troubleshooting guide</a> or <a href="https://github.com/subdavis/Tusk/issues">open an issue</a>.
</p>

<a class="waves-effect waves-light btn mr-10" @click="tabRouter.route('/help/me/choose')">Help me choose</a>
<a class="waves-effect waves-light btn" @click="tabRouter.route('/new/user')">I don't have a KeePass Database</a>
Expand Down
9 changes: 6 additions & 3 deletions src/components/Unlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export default {
this.unlockedState.clearClipboardState()
this.unlockedState.clearCache() // new
},
showResults(entries) {
showResults(entries, fromCache) {
let getMatchesForThreshold = (threshold, entries, requireEmptyURL = false) => {
return entries.filter(e => (e.matchRank >= threshold) && (requireEmptyURL ? !e.URL : true));
}
Expand Down Expand Up @@ -185,7 +185,10 @@ export default {
this.unlockedState.cacheSet('priorityEntries', priorityEntries)
this.$forceUpdate()
//save longer term (in encrypted storage)
this.secureCache.save('secureCache.entries', entries);
if (!fromCache) {
// Don't bother saving if we're just reading from the cache.
this.secureCache.save('secureCache.entries', entries);
}
this.busy = false
})
},
Expand Down Expand Up @@ -291,7 +294,7 @@ export default {
try {
let entries = await this.secureCache.get('secureCache.entries');
if (entries !== undefined && entries.length > 0) {
this.showResults(entries)
this.showResults(entries, true)
} else {
try_autounlock()
}
Expand Down
2 changes: 1 addition & 1 deletion tests/services/test.protectedMemory.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const should = require('should')

import { ProtectedMemory } from '$services/protectedMemory.js'
import { ProtectedMemory } from '$services/protectedMemory'

describe('Protected Memory', function() {

Expand Down
2 changes: 1 addition & 1 deletion tests/services/test.secureCache.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const should = require('should')

import { ProtectedMemory } from '$services/protectedMemory.js'
import { ProtectedMemory } from '$services/protectedMemory'
import { SecureCacheMemory } from '$services/secureCacheMemory.js'

var memory = {};
Expand Down
2 changes: 1 addition & 1 deletion tests/test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Services
require('./services/test.keepassReference.js')
require('./services/test.protectedMemory.js')
require('./services/test.protectedMemory')
require('./services/test.secureCache.js')
require('./services/test.settings.js')

Expand Down