From e227f9aaf7938868087eb475fa1b0cccf8e16473 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 28 Aug 2022 10:00:51 +0300 Subject: [PATCH] initial commit --- .gitignore | 1 + extension/background.js | 169 +++++++++++++++++++ extension/foreground.js | 229 ++++++++++++++++++++++++++ extension/icons/icon.png | Bin 0 -> 1656 bytes extension/manifest.json | 28 ++++ extension/popup.html | 13 ++ extension/popup.js | 1 + host/ee.lunasqu.password_manager.json | 7 + host/package-lock.json | 24 +++ host/package.json | 5 + host/server.js | 79 +++++++++ 11 files changed, 556 insertions(+) create mode 100644 .gitignore create mode 100644 extension/background.js create mode 100644 extension/foreground.js create mode 100644 extension/icons/icon.png create mode 100644 extension/manifest.json create mode 100644 extension/popup.html create mode 100644 extension/popup.js create mode 100644 host/ee.lunasqu.password_manager.json create mode 100644 host/package-lock.json create mode 100644 host/package.json create mode 100755 host/server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000..d1f47cf --- /dev/null +++ b/extension/background.js @@ -0,0 +1,169 @@ +const tabForms = {}; +const tabMatches = {}; + +let lastTabId = null; +let currentPageForms = null; +let currentPageMatches = null; +let addedContexts = []; + +function askNative(msg) { + const port = chrome.runtime.connectNative('ee.lunasqu.password_manager'); + + return new Promise((resolve, reject) => { + let gotResponse = false; + port.onMessage.addListener((msg) => { + gotResponse = true; + resolve(msg); + port.disconnect(); + }); + + port.onDisconnect.addListener((msg) => { + if (!gotResponse) { + gotResponse = true; + reject(msg); + } + }); + + port.postMessage(msg); + }); +} + +chrome.runtime.onInstalled.addListener(() => { + chrome.contextMenus.create({ + id: 'fillContextMenu', + title: 'Autofill password', + contexts: ['editable'], + }); +}); + +function addContextMenuItems() { + if (!currentPageMatches) { + return; + } + + clearContextMenu(); + for (const match of currentPageMatches) { + const ctxID = `fill-${match}`; + chrome.contextMenus.create({ + id: ctxID, + title: match, + contexts: ['editable'], + parentId: 'fillContextMenu', + }); + addedContexts.push(ctxID); + } +} + +function clearContextMenu() { + addedContexts.forEach((id) => { + try { + chrome.contextMenus.remove(id); + } catch (e) { + console.error(e); + } + }); + addedContexts.length = 0; +} + +async function injectForeground() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + + if (!tab || !tab.url) { + return; + } + + lastTabId = tab.id; + + await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + files: ['./foreground.js'], + }); + + const parsed = new URL(tab.url); + + askNative({ + domain: parsed.host, + }).then(({ results }) => { + if (results && results.length) { + currentPageMatches = results; + tabMatches[tab.id] = results; + chrome.tabs.sendMessage(tab.id, { + message: 'has_entries', + payload: results, + }); + addContextMenuItems(); + } + }); +} + +async function sendPasswordToTab(rdata, existingTab) { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + const realTab = existingTab || tab; + + if (!realTab || !realTab.url) { + return; + } + + chrome.tabs.sendMessage(realTab.id, { + message: 'fill_password', + payload: rdata, + }); +} + +chrome.webNavigation.onCompleted.addListener((ee) => { + if (ee.frameType !== 'outermost_frame') { + return; + } + currentPageForms = null; + currentPageMatches = null; + injectForeground(); +}); + +chrome.tabs.onActivated.addListener(function (activeInfo) { + lastTabId = activeInfo.tabId; + currentPageForms = tabForms[activeInfo.tabId]; + currentPageMatches = tabMatches[activeInfo.tabId]; + + clearContextMenu(); + if (currentPageMatches) { + addContextMenuItems(); + } +}); + +chrome.tabs.onRemoved.addListener(function (tabId, info) { + delete tabForms[tabId]; + delete tabMatches[tabId]; +}); + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.message === 'login_forms') { + if (sender && sender.tab && sender.tab.id) { + tabForms[sender.tab.id] = request.payload; + } + + if (sender.tab.id === lastTabId) { + currentPageForms = request.payload; + } + sendResponse(true); + } + + if (request.message === 'autofill') { + askNative({ + getPassword: request.payload, + }).then((response) => { + sendPasswordToTab(response); + }); + sendResponse(true); + } +}); + +chrome.contextMenus.onClicked.addListener((info, tab) => { + if (info && info.menuItemId && info.menuItemId.startsWith('fill-')) { + const fillId = info.menuItemId.substring(5); + askNative({ + getPassword: fillId, + }).then((response) => { + sendPasswordToTab(response, tab); + }); + } +}); diff --git a/extension/foreground.js b/extension/foreground.js new file mode 100644 index 0000000..388924a --- /dev/null +++ b/extension/foreground.js @@ -0,0 +1,229 @@ +(() => { + let currentPageMatches = null; + let currentForm = null; + let successfulAttachment = false; + let focusedField = null; + + function generateQuerySelector(el) { + if (el.tagName.toLowerCase() === 'html') { + return 'HTML'; + } + + let str = el.tagName; + str += el.id != '' ? '#' + el.id : ''; + if (el.className) { + const classes = el.className.split(/\s/); + for (let i = 0; i < classes.length; i++) { + str += '.' + classes[i]; + } + } + + return generateQuerySelector(el.parentNode) + ' > ' + str; + } + + function lookForLoginForms() { + const allPasswordInputs = document.querySelectorAll( + 'input[type="password"]' + ); + let loginForms = []; + + allPasswordInputs.forEach((field) => { + const closestForm = field.closest('form'); + if (!closestForm) { + return; + } + + const existing = loginForms.find(({ form }) => form === closestForm); + if (existing) { + existing.passwordAgain = field; + return; + } + + let usernameField; + const allInputs = Array.from(closestForm.querySelectorAll('input')); + const contenders = allInputs.filter((element) => { + const nameAttr = (element.getAttribute('name') || '').toLowerCase(); + const idAttr = (element.getAttribute('id') || '').toLowerCase(); + const closestLabel = closestForm.querySelector( + `label[for="${idAttr}"]` + ); + + if (closestLabel) { + const labelText = closestLabel.innerText.toLowerCase().trim(); + if ( + labelText.startsWith('user') || + labelText.startsWith('email') || + labelText.startsWith('e-mail') + ) { + return true; + } + } + + if (nameAttr.includes('username')) { + return true; + } + + if (nameAttr.includes('email')) { + return true; + } + + if (nameAttr.includes('name')) { + return true; + } + }); + + usernameField = contenders[0]; + + loginForms.push({ + password: field, + username: usernameField, + form: closestForm, + }); + }); + + if (loginForms.length) { + chrome.runtime.sendMessage({ + message: 'login_forms', + payload: loginForms.map((stringify) => ({ + form: generateQuerySelector(stringify.form), + password: generateQuerySelector(stringify.password), + passwordAgain: stringify.passwordAgain + ? generateQuerySelector(stringify.passwordAgain) + : null, + username: stringify.username + ? generateQuerySelector(stringify.username) + : null, + })), + }); + } + + return loginForms; + } + + function createOptionsSelect(commitAutofill) { + const select = document.createElement('select'); + const unopt = document.createElement('option'); + unopt.innerText = 'Select autofill...'; + select.appendChild(unopt); + + Object.assign(select.style, { + pointerEvents: 'all', + }); + + currentPageMatches.forEach((match) => { + const option = document.createElement('option'); + option.value = match; + option.innerText = match; + select.appendChild(option); + }); + + select.addEventListener('change', function () { + const val = select.value; + if (val) { + commitAutofill(val); + } + }); + return select; + } + + function attachLoginFormHighlight(info) { + successfulAttachment = true; + const autoFillContainer = document.createElement('div'); + Object.assign(autoFillContainer.style, { + position: 'absolute', + outline: '10px solid rgb(0 170 255 / 60%)', + boxSizing: 'border-box', + pointerEvents: 'none', + borderRadius: '10px', + zIndex: '10000000', + }); + document.body.appendChild(autoFillContainer); + + function reposition() { + const boundingBox = info.form.getBoundingClientRect(); + Object.assign(autoFillContainer.style, { + top: `${boundingBox.y - 10 + window.scrollY}px`, + left: `${boundingBox.x - 10}px`, + width: `${boundingBox.width + 20}px`, + height: `${boundingBox.height + 20}px`, + }); + } + + window.addEventListener('resize', reposition); + window.addEventListener('scroll', reposition); + reposition(); + + const select = createOptionsSelect((password) => { + currentForm = info; + chrome.runtime.sendMessage({ + message: 'autofill', + payload: password, + }); + }); + autoFillContainer.appendChild(select); + } + + function init() { + const forms = lookForLoginForms(); + if (forms.length) { + forms.forEach((form) => attachLoginFormHighlight(form)); + } + } + + function fakeTriggers(input, value) { + const inputEvt = new Event('input'); + const changeEvt = new Event('change'); + input.focus(); + input.value = value; + input.setAttribute('value', value); + input.dispatchEvent(inputEvt); + input.dispatchEvent(changeEvt); + input.blur(); + } + + chrome.runtime.onMessage.addListener(function ( + request, + sender, + sendResponse + ) { + if (request.message === 'has_entries') { + currentPageMatches = request.payload; + init(); + sendResponse(true); + } + + if (request.message === 'fill_password') { + if (request && request.payload.password) { + if (currentForm) { + fakeTriggers(currentForm.password, request.payload.password); + + if (currentForm.username && request.payload.username) { + fakeTriggers(currentForm.username, request.payload.username); + } + + setTimeout(() => currentForm.password.focus(), 100); + } else if (focusedField) { + fakeTriggers(focusedField, request.payload.password); + } + } + + sendResponse(true); + } + }); + + document.addEventListener('click', function (e) { + if (!currentPageMatches) { + return; + } + + focusedField = e.target; + + if (successfulAttachment) { + return; + } + + if (e.target && e.target.tagName === 'INPUT') { + init(); + } + }); +})(); diff --git a/extension/icons/icon.png b/extension/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b4ffeadc9aaec5e5b9704eacc0a2312b07b238 GIT binary patch literal 1656 zcmV-;28a2HP)EX>4Tx04R}tkv&MmKpe$iTct%RB6bizAVYPsAS&XhRVYG*P%E_RU~=gfG-*gu zTpR`0f`cE6RR%KKJM7R&pi-d;;+-(+!JwgLrz= z(mC%FM_5r(h|h_~47wokBiCh@-#8Z>_Vdh$kxtDMM~H<&8_R9XiiS!&MI2RBjq?2& zmle)ioYiubHSft^7|v-c%Uq{9gaj6`1Q7ycR8c}17Gkt&q?kz2e%!-9;P^#y$>b`5 zkz)ZBsE`~#_#gc4*33^%xJltS(D`E9A0t3;7iiRM`}^3o8z(^E8Mx9~{z@H~`6Rv8 z(xOK|&o*#z-O}Ve;Bp7(f6^sGa-;xFe?AYqpV2pEfxcUyYt8MgxsTHaAWdB*Z-9eC zV6;Hl>mKh8wfFY#nPz`KL6LHk)8hZJ00006VoOIv00000008+zyMF)x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru<_H@R9xvI|0-*o^02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00dA;L_t(&-qo6IOj~6X$A9-y*kpmkWx?!)Scqt-y2YhV zGdp~VQ9n#J_EHJUqA^ZFj8lVdnF|?;8A@cqX=X?hM)ZTTE@`G_Sr$Z-mib{!iJ71Q zHx^`!&2>b^%hY)NaGr)z+e-(f?foaDH}^a(=YP+0p7VA!5&^GK0IUO?fCI1tdB7sI zVhp$p^aI^MC*a4eouBK!*5;t}a-bY210I^?uXF%D;0SJQFgXOgMlnzcJP+iM7>oif zKr?QwXMPBHjclMAcokU8{2&N40gbq|@x&4E8jk^cfX7J=+JJi8TKn7yc#R!E4UkWA zF#y!z);zN(;5BvuwZJW;1`|LnZmr?w35fD<0SiejCV_piNN6V8=7SVI`Dm7SCE9k< zVmNGT3if~ITIT0nV}j+N6bAcrqjW6~-$G?r4`s52q!>AeHBqRm zO3JEOr>ip1s&Lv=4Qr0pYB+uXJj2l8g>MdneI=~l2?h7bnvr4XIte`i=<9^r?}as` z;9L*3ELqe469hhkZa-w_La__hY=nG=xhysux+=iq!+V=xs2A>h0Q{}6do^6UB(i`0 z0?$j3VS}}s;o`4wU^ARLD)K+Cz^+yB<-4NX3J09{7_JA+W#+KkRQDi0lx%~is=zQ{ z@iO?L27dkyT;;GN9}W*gmP%lqPs1B4#VREg@b?Y5ztjO6UWeQzP*Ep@M}}j|%#o*u zwLWG>!73I0G`Q?Ju(h@nC^TKxbrO2NhnjX63&NSN!MPEtn&GxWSdb0(JPMN&(BB0`_d~@ykg*W1 z{tl097r)6`0Kc7)t+BKSHtmJATV$`tl$kp6j~ZU%An7#!SrL%oLW_k6w1fsC z;i*KkYFVT^f=btj#+LM1nk+z|DSV0(nank+_FcN7P3cH<0@8D=M;fW=h5=-1Gaj3O zkZ1?$kd}73U;>%ev`0&)i9*7IOlBt2K>0~zO5=%D4-kVy1F$c}mYBd~IwOs(nPBN5 z-xLW8k6EWkSa{GqorpzWHTXXfi@xj;olMGJxA_l(9CVRyw(CCt0000 + + + + + + Popup + + +

test

+ + + diff --git a/extension/popup.js b/extension/popup.js new file mode 100644 index 0000000..c4e85a2 --- /dev/null +++ b/extension/popup.js @@ -0,0 +1 @@ +// chrome.tabs.sendMessage(tabId, "message", function (response)); diff --git a/host/ee.lunasqu.password_manager.json b/host/ee.lunasqu.password_manager.json new file mode 100644 index 0000000..97d4353 --- /dev/null +++ b/host/ee.lunasqu.password_manager.json @@ -0,0 +1,7 @@ +{ + "name": "ee.lunasqu.password_manager", + "description": "Password manager native messager", + "path": "server.js", + "type": "stdio", + "allowed_origins": [ "chrome-extension://hafjmpnminafbnikdojlbafngmeoplik/" ] +} diff --git a/host/package-lock.json b/host/package-lock.json new file mode 100644 index 0000000..f92310d --- /dev/null +++ b/host/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "password-store-extension", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "chrome-native-messaging": "^0.2.0" + } + }, + "node_modules/chrome-native-messaging": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chrome-native-messaging/-/chrome-native-messaging-0.2.0.tgz", + "integrity": "sha512-RgTFMY0x/K5JUSLfd+rTaWxNvkliYqNR41h5DbbkjlDCbxmZSXIHAYOSTbIazjyorzpK26Y2H9BXnboy4s9SXw==" + } + }, + "dependencies": { + "chrome-native-messaging": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chrome-native-messaging/-/chrome-native-messaging-0.2.0.tgz", + "integrity": "sha512-RgTFMY0x/K5JUSLfd+rTaWxNvkliYqNR41h5DbbkjlDCbxmZSXIHAYOSTbIazjyorzpK26Y2H9BXnboy4s9SXw==" + } + } +} diff --git a/host/package.json b/host/package.json new file mode 100644 index 0000000..b08421e --- /dev/null +++ b/host/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "chrome-native-messaging": "^0.2.0" + } +} diff --git a/host/server.js b/host/server.js new file mode 100755 index 0000000..0313e16 --- /dev/null +++ b/host/server.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node +const nativeMessage = require('chrome-native-messaging'); +const childProcess = require('child_process'); +const fs = require('fs/promises'); +const path = require('path'); + +const passwds = path.join(process.env.HOME, '.password-store'); + +async function recurseDir(dir) { + const array = []; + const paths = await fs.readdir(dir); + const root = path.basename(dir); + for (const file of paths) { + const stat = await fs.stat(path.join(dir, file)); + if (file.startsWith('.')) { + continue; + } + + if (stat.isDirectory()) { + const children = await recurseDir(path.join(dir, file)); + array.push(...children); + } else { + array.push(path.join(dir, file.replace('.gpg', ''))); + } + } + return array; +} + +async function findSiteInPasswords(siteDomain) { + const allPasswords = await recurseDir(passwds); + let sdwTLD = siteDomain.split('.'); + sdwTLD = sdwTLD.slice(sdwTLD.length - 2, sdwTLD.length - 1).join('.'); + + return allPasswords + .map((file) => file.substring(passwds.length + 1)) + .filter((item) => { + const name = item.toLowerCase(); + return name.includes(siteDomain) || (sdwTLD && name.includes(sdwTLD)); + }); +} + +async function getPassword(password) { + return new Promise((resolve, reject) => { + childProcess.exec(`pass ${password}`, (error, stdout, stderr) => { + if (stderr && stderr.length) { + return resolve(null); + } + + resolve(stdout.toString().split('\n')); + }); + }); +} + +process.stdin + .pipe(new nativeMessage.Input()) + .pipe( + new nativeMessage.Transform(function (msg, push, done) { + if (msg.domain) { + findSiteInPasswords(msg.domain).then((results) => { + push({ results }); + done(); + }); + return; + } + + if (msg.getPassword) { + getPassword(msg.getPassword).then(([password, username]) => { + push({ password, username }); + done(); + }); + return; + } + + push({ hello: true }); + done(); + }) + ) + .pipe(new nativeMessage.Output()) + .pipe(process.stdout);