From 528bef3403c8b08b65ec6dfc21abc71ebf48aa17 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Fri, 7 Jan 2022 03:57:08 +0800 Subject: [PATCH] add autofill command --- manifests/manifest-chrome.json | 3 + manifests/manifest-edge.json | 3 + manifests/manifest-firefox.json | 3 + src/background.ts | 55 +++++++++++++ src/store/Accounts.ts | 139 +------------------------------ src/utils.ts | 141 ++++++++++++++++++++++++++++++++ 6 files changed, 208 insertions(+), 136 deletions(-) create mode 100644 src/utils.ts diff --git a/manifests/manifest-chrome.json b/manifests/manifest-chrome.json index 2a6a166ef..45c90566f 100644 --- a/manifests/manifest-chrome.json +++ b/manifests/manifest-chrome.json @@ -22,6 +22,9 @@ "_execute_browser_action": {}, "scan-qr": { "description": "Scan a QR code" + }, + "autofill": { + "description": "Autofill the matched code" } }, "options_ui": { diff --git a/manifests/manifest-edge.json b/manifests/manifest-edge.json index 20ff01223..4d9faa51a 100644 --- a/manifests/manifest-edge.json +++ b/manifests/manifest-edge.json @@ -22,6 +22,9 @@ "_execute_browser_action": {}, "scan-qr": { "description": "Scan a QR code" + }, + "autofill": { + "description": "Autofill the matched code" } }, "options_ui": { diff --git a/manifests/manifest-firefox.json b/manifests/manifest-firefox.json index 48151a940..6f03d7c2b 100644 --- a/manifests/manifest-firefox.json +++ b/manifests/manifest-firefox.json @@ -34,6 +34,9 @@ "_execute_browser_action": {}, "scan-qr": { "description": "Scan a QR code" + }, + "autofill": { + "description": "Autofill the matched code" } }, "options_ui": { diff --git a/src/background.ts b/src/background.ts index 9e4e99450..cb26d155f 100644 --- a/src/background.ts +++ b/src/background.ts @@ -8,6 +8,8 @@ import { Encryption } from "./models/encryption"; import { EntryStorage, ManagedStorage } from "./models/storage"; import { Dropbox, Drive, OneDrive } from "./models/backup"; import * as uuid from "uuid/v4"; +import { getSiteName, getMatchedEntries } from "./utils"; +import { CodeState } from "./models/otp"; import { getOTPAuthPerLineFromOPTAuthMigration } from "./models/migration"; @@ -560,6 +562,59 @@ chrome.commands.onCommand.addListener(async (command: string) => { }); break; + case "autofill": + await new Promise( + (resolve: () => void, reject: (reason: Error) => void) => { + try { + return chrome.tabs.executeScript( + { file: "/dist/content.js" }, + () => { + chrome.tabs.insertCSS({ file: "/css/content.css" }, resolve); + } + ); + } catch (error) { + console.error(error); + return reject(error); + } + } + ); + + if (cachedPassphrase === "") { + return; + } + + chrome.tabs.query( + { active: true, lastFocusedWindow: true }, + async (tabs) => { + const tab = tabs[0]; + if (!tab || !tab.id) { + return; + } + contentTab = tab; + + const siteName = await getSiteName(); + const entries = await EntryStorage.get(); + const matchedEntries = getMatchedEntries(siteName, entries); + + if (matchedEntries && matchedEntries.length === 1) { + const entry = matchedEntries[0]; + const encryption = new Encryption(cachedPassphrase); + entry.applyEncryption(encryption); + + if ( + entry.code !== CodeState.Encrypted && + entry.code !== CodeState.Invalid + ) { + chrome.tabs.sendMessage(tab.id, { + action: "pastecode", + code: matchedEntries[0].code, + }); + } + } + } + ); + break; + default: break; } diff --git a/src/store/Accounts.ts b/src/store/Accounts.ts index f74d1d198..6369b6665 100644 --- a/src/store/Accounts.ts +++ b/src/store/Accounts.ts @@ -3,6 +3,7 @@ import { Encryption } from "../models/encryption"; import * as CryptoJS from "crypto-js"; import { OTPType, OTPAlgorithm } from "../models/otp"; import { ActionContext } from "vuex"; +import { getSiteName, getMatchedEntriesHash } from "../utils"; export class Accounts implements Module { async getModule() { @@ -22,7 +23,7 @@ export class Accounts implements Module { sectorOffset: 0, // Offset in seconds for animations second: 0, // Offset in seconds for math filter: true, - siteName: await this.getSiteName(), + siteName: await getSiteName(), showSearch: false, exportData: await EntryStorage.getExport(entries), exportEncData: await EntryStorage.getExport(entries, true), @@ -41,7 +42,7 @@ export class Accounts implements Module { ); }, matchedEntries: (state: AccountsState) => { - return this.matchedEntries(state.siteName, state.entries); + return getMatchedEntriesHash(state.siteName, state.entries); }, currentlyEncrypted(state: AccountsState) { for (const entry of state.entries) { @@ -513,82 +514,6 @@ export class Accounts implements Module { }; } - private async getSiteName() { - return new Promise((resolve: (value: Array) => void) => { - chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { - const tab = tabs[0]; - const query = new URLSearchParams( - document.location.search.substring(1) - ); - - let title: string | null; - let url: string | null; - const titleFromQuery = query.get("title"); - const urlFromQuery = query.get("url"); - - if (titleFromQuery && urlFromQuery) { - title = decodeURIComponent(titleFromQuery); - url = decodeURIComponent(urlFromQuery); - } else { - if (!tab) { - return resolve([null, null]); - } - - title = tab.title?.replace(/[^a-z0-9]/gi, "").toLowerCase() ?? null; - url = tab.url ?? null; - } - - if (!url) { - return resolve([title, null]); - } - - const urlParser = new URL(url); - const hostname = urlParser.hostname; // it's always lower case - - // try to parse name from hostname - // i.e. hostname is www.example.com - // name should be example - let nameFromDomain = ""; - - // ip address - if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { - nameFromDomain = hostname; - } - - // local network - if (hostname.indexOf(".") === -1) { - nameFromDomain = hostname; - } - - const hostLevelUnits = hostname.split("."); - - if (hostLevelUnits.length === 2) { - nameFromDomain = hostLevelUnits[0]; - } - - // www.example.com - // example.com.cn - if (hostLevelUnits.length > 2) { - // example.com.cn - if ( - ["com", "net", "org", "edu", "gov", "co"].indexOf( - hostLevelUnits[hostLevelUnits.length - 2] - ) !== -1 - ) { - nameFromDomain = hostLevelUnits[hostLevelUnits.length - 3]; - } else { - // www.example.com - nameFromDomain = hostLevelUnits[hostLevelUnits.length - 2]; - } - } - - nameFromDomain = nameFromDomain.replace(/-/g, "").toLowerCase(); - - return resolve([title, nameFromDomain, hostname]); - }); - }); - } - private getCachedPassphrase() { return new Promise((resolve: (value: string) => void) => { chrome.runtime.sendMessage( @@ -604,62 +529,4 @@ export class Accounts implements Module { const otpEntries = await EntryStorage.get(); return otpEntries; } - - private matchedEntries( - siteName: Array, - entries: OTPEntryInterface[] - ) { - if (siteName.length < 2) { - return false; - } - - const matched = []; - - for (const entry of entries) { - if (this.isMatchedEntry(siteName, entry)) { - matched.push(entry.hash); - } - } - - return matched; - } - - private isMatchedEntry( - siteName: Array, - entry: OTPEntryInterface - ) { - if (!entry.issuer) { - return false; - } - - const issuerHostMatches = entry.issuer.split("::"); - const issuer = issuerHostMatches[0] - .replace(/[^0-9a-z]/gi, "") - .toLowerCase(); - - if (!issuer) { - return false; - } - - const siteTitle = siteName[0] || ""; - const siteNameFromHost = siteName[1] || ""; - const siteHost = siteName[2] || ""; - - if (issuerHostMatches.length > 1) { - if (siteHost && siteHost.indexOf(issuerHostMatches[1]) !== -1) { - return true; - } - } - // site title should be more detailed - // so we use siteTitle.indexOf(issuer) - if (siteTitle && siteTitle.indexOf(issuer) !== -1) { - return true; - } - - if (siteNameFromHost && issuer.indexOf(siteNameFromHost) !== -1) { - return true; - } - - return false; - } } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..6941cc24b --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,141 @@ +export async function getSiteName() { + return new Promise((resolve: (value: Array) => void) => { + chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { + const tab = tabs[0]; + const query = new URLSearchParams(document.location.search.substring(1)); + + let title: string | null; + let url: string | null; + const titleFromQuery = query.get("title"); + const urlFromQuery = query.get("url"); + + if (titleFromQuery && urlFromQuery) { + title = decodeURIComponent(titleFromQuery); + url = decodeURIComponent(urlFromQuery); + } else { + if (!tab) { + return resolve([null, null]); + } + + title = tab.title?.replace(/[^a-z0-9]/gi, "").toLowerCase() ?? null; + url = tab.url ?? null; + } + + if (!url) { + return resolve([title, null]); + } + + const urlParser = new URL(url); + const hostname = urlParser.hostname; // it's always lower case + + // try to parse name from hostname + // i.e. hostname is www.example.com + // name should be example + let nameFromDomain = ""; + + // ip address + if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { + nameFromDomain = hostname; + } + + // local network + if (hostname.indexOf(".") === -1) { + nameFromDomain = hostname; + } + + const hostLevelUnits = hostname.split("."); + + if (hostLevelUnits.length === 2) { + nameFromDomain = hostLevelUnits[0]; + } + + // www.example.com + // example.com.cn + if (hostLevelUnits.length > 2) { + // example.com.cn + if ( + ["com", "net", "org", "edu", "gov", "co"].indexOf( + hostLevelUnits[hostLevelUnits.length - 2] + ) !== -1 + ) { + nameFromDomain = hostLevelUnits[hostLevelUnits.length - 3]; + } else { + // www.example.com + nameFromDomain = hostLevelUnits[hostLevelUnits.length - 2]; + } + } + + nameFromDomain = nameFromDomain.replace(/-/g, "").toLowerCase(); + + return resolve([title, nameFromDomain, hostname]); + }); + }); +} + +export function getMatchedEntries( + siteName: Array, + entries: OTPEntryInterface[] +) { + if (siteName.length < 2) { + return false; + } + + const matched = []; + + for (const entry of entries) { + if (isMatchedEntry(siteName, entry)) { + matched.push(entry); + } + } + + return matched; +} + +export function getMatchedEntriesHash( + siteName: Array, + entries: OTPEntryInterface[] +) { + const matchedEnteries = getMatchedEntries(siteName, entries); + if (matchedEnteries) { + return matchedEnteries.map((entry) => entry.hash); + } + + return false; +} + +function isMatchedEntry( + siteName: Array, + entry: OTPEntryInterface +) { + if (!entry.issuer) { + return false; + } + + const issuerHostMatches = entry.issuer.split("::"); + const issuer = issuerHostMatches[0].replace(/[^0-9a-z]/gi, "").toLowerCase(); + + if (!issuer) { + return false; + } + + const siteTitle = siteName[0] || ""; + const siteNameFromHost = siteName[1] || ""; + const siteHost = siteName[2] || ""; + + if (issuerHostMatches.length > 1) { + if (siteHost && siteHost.indexOf(issuerHostMatches[1]) !== -1) { + return true; + } + } + // site title should be more detailed + // so we use siteTitle.indexOf(issuer) + if (siteTitle && siteTitle.indexOf(issuer) !== -1) { + return true; + } + + if (siteNameFromHost && issuer.indexOf(siteNameFromHost) !== -1) { + return true; + } + + return false; +}