diff --git a/README.md b/README.md index 643c80d..130b5fd 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ In order to use Browserpass you must also install a [companion native messaging - [Modal HTTP authentication](#modal-http-authentication) - [Password store locations](password-store-locations) - [Options](#options) +- [Usage data](#usage-data) - [Security](#security) - [Privacy](#privacy) - [Requested permissions](#requested-permissions) @@ -215,6 +216,12 @@ Browserpass allows configuring certain settings in different places places using - Custom store locations - badge background color (aka `bgColor`) - Custom store locations - badge text color (aka `color`) +## Usage data + +Browserpass keeps metadata of recently used credentials in local storage and Indexed DB of the background page. This is first and foremost internal data to make Browserpass function properly, used for example to implement the [Password matching and sorting](#password-matching-and-sorting) algorithm, but nevertheless you might find it useful to explore using your browser's devtools. For example, if you are considering to rotate all passwords that you used in the past month (e.g. if you just found out that you had a malicious app installed for several weeks), you can retrieve such list from Indexed DB quite easily (open an issue if you need help). + +For details on how we treat your data and how to remove it, consult [Security](#security) and [Privacy](#privacy) sections. + ## Security Browserpass aims to protect your passwords and computer from malicious or fraudulent websites. @@ -229,6 +236,10 @@ Browserpass aims to protect your passwords and computer from malicious or fraudu Browserpass does not send any telemetry data. All metadata that is collected in order for the extension to function correctly is stored _only_ in local storage, and never leaves your browser. +This data is not synchronized between your computers, and upon removing Browserpass extension all the data will be automatically purged by your browser. + +In order to remove all metadata, use the "Clear usage data" button in the extension options page or do it using your browser's devtools. + ## Requested permissions Browserpass extension requests the following permissions: diff --git a/src/background.js b/src/background.js index 2ab4ae6..f9b7d03 100644 --- a/src/background.js +++ b/src/background.js @@ -4,6 +4,7 @@ require("chrome-extension-async"); var TldJS = require("tldjs"); var sha1 = require("sha1"); +var idb = require("idb"); // native application id var appID = "com.github.browserpass.native"; @@ -163,7 +164,7 @@ function copyToClipboard(text) { * @param bool remove Remove this item from recent history * @return void */ -function saveRecent(settings, login, remove = false) { +async function saveRecent(settings, login, remove = false) { var ignoreInterval = 60000; // 60 seconds - don't increment counter twice within this window // save store timestamp @@ -183,6 +184,15 @@ function saveRecent(settings, login, remove = false) { if (!login.inCurrentDomain && login.recent.count === 1) { updateMatchingPasswordsCount(settings.tab.id); } + + // save to usage log + const DB_VERSION = 1; + const db = await idb.openDB("browserpass", DB_VERSION, { + upgrade(db) { + db.createObjectStore("log", { keyPath: "time" }); + } + }); + await db.add("log", { time: Date.now(), host: settings.host, login: login.login }); } /** @@ -624,7 +634,7 @@ async function handleMessage(settings, message, sendResponse) { case "copyPassword": try { copyToClipboard(message.login.fields.secret); - saveRecent(settings, message.login); + await saveRecent(settings, message.login); sendResponse({ status: "ok" }); } catch (e) { sendResponse({ @@ -636,7 +646,7 @@ async function handleMessage(settings, message, sendResponse) { case "copyUsername": try { copyToClipboard(message.login.fields.login); - saveRecent(settings, message.login); + await saveRecent(settings, message.login); sendResponse({ status: "ok" }); } catch (e) { sendResponse({ @@ -689,7 +699,7 @@ async function handleMessage(settings, message, sendResponse) { // dispatch initial fill request var filledFields = await fillFields(settings, message.login, fields); - saveRecent(settings, message.login); + await saveRecent(settings, message.login); // no need to check filledFields, because fillFields() already throws an error if empty sendResponse({ status: "ok", filledFields: filledFields }); @@ -707,6 +717,17 @@ async function handleMessage(settings, message, sendResponse) { } } break; + case "clearUsageData": + try { + await clearUsageData(); + sendResponse({ status: "ok" }); + } catch (e) { + sendResponse({ + status: "error", + message: e.message + }); + } + break; default: sendResponse({ status: "error", @@ -859,6 +880,27 @@ async function receiveMessage(message, sender, sendResponse) { } } +/** + * Clear usage data + * + * @since 3.0.10 + * + * @return void + */ +async function clearUsageData() { + // clear local storage + localStorage.removeItem("foreignFills"); + localStorage.removeItem("recent"); + Object.keys(localStorage).forEach(key => { + if (key.startsWith("recent:")) { + localStorage.removeItem(key); + } + }); + + // clear Indexed DB + await idb.deleteDB("browserpass"); +} + /** * Save settings if they are valid * diff --git a/src/options/interface.js b/src/options/interface.js index 1411fd8..8921bda 100644 --- a/src/options/interface.js +++ b/src/options/interface.js @@ -9,9 +9,10 @@ var m = require("mithril"); * * @param object settings Settings object * @param function saveSettings Function to save settings + * @param function clearUsageData Function to clear usage data * @return void */ -function Interface(settings, saveSettings) { +function Interface(settings, saveSettings, clearUsageData) { // public methods this.attach = attach; this.view = view; @@ -20,6 +21,7 @@ function Interface(settings, saveSettings) { this.settings = settings; this.saveSettings = saveSettings; this.saveEnabled = false; + this.clearUsageData = clearUsageData; } /** @@ -97,6 +99,24 @@ function view(ctl, params) { "Save" ) ); + + nodes.push( + m( + "button.clearUsageData", + { + onclick: async () => { + try { + await this.clearUsageData(); + this.error = undefined; + } catch (e) { + this.error = e; + } + m.redraw(); + } + }, + "Clear usage data" + ) + ); return nodes; } diff --git a/src/options/options.js b/src/options/options.js index ff5e165..8e1b6df 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -67,6 +67,20 @@ async function saveSettings(settings) { return await getSettings(); } +/** + * Clear usage data + * + * @since 3.0.10 + * + * @return void + */ +async function clearUsageData() { + var response = await chrome.runtime.sendMessage({ action: "clearUsageData" }); + if (response.status != "ok") { + throw new Error(response.message); + } +} + /** * Run the main options logic * @@ -76,7 +90,7 @@ async function saveSettings(settings) { */ async function run() { try { - var options = new Interface(await getSettings(), saveSettings); + var options = new Interface(await getSettings(), saveSettings, clearUsageData); options.attach(document.body); } catch (e) { handleError(e); diff --git a/src/options/options.less b/src/options/options.less index cbe9de8..ab8cc38 100644 --- a/src/options/options.less +++ b/src/options/options.less @@ -90,8 +90,8 @@ h3:first-child { .add-store { cursor: pointer; - display: inline-block; - margin-top: 8px; + display: block; + margin-top: 12px; margin-bottom: 30px; color: rgb(17, 85, 204); text-decoration: underline; @@ -104,7 +104,7 @@ h3:first-child { .save { cursor: pointer; - display: block; + margin-right: 10px; } @-moz-document url-prefix() { diff --git a/src/package.json b/src/package.json index c279d95..9b2e0f3 100644 --- a/src/package.json +++ b/src/package.json @@ -17,6 +17,7 @@ "dependencies": { "chrome-extension-async": "^3.3.2", "fuzzysort": "^1.1.4", + "idb": "^4.0.3", "mithril": "^1.1.0", "moment": "^2.24.0", "sha1": "^1.1.1", diff --git a/src/yarn.lock b/src/yarn.lock index 2a28aaf..80601f2 100644 --- a/src/yarn.lock +++ b/src/yarn.lock @@ -696,6 +696,11 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +idb@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/idb/-/idb-4.0.3.tgz#b454a51ba0c45038a9a7be6898ca996aa74f9a37" + integrity sha512-moRlNNe0Gsvp4jAwz5cJ7scjyNTVA/cESKGCobULaljfaKZ970y8NDNCseHdMY+YxNXH58Z1V+7tTyf0GZyKqw== + ieee754@^1.1.4: version "1.1.12" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"