initial commit

This commit is contained in:
Evert Prants 2022-08-28 10:00:51 +03:00
commit e227f9aaf7
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
11 changed files with 556 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

169
extension/background.js Normal file
View File

@ -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);
});
}
});

229
extension/foreground.js Normal file
View File

@ -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();
}
});
})();

BIN
extension/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

28
extension/manifest.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "Password manager",
"description": "Password manager",
"version": "1.0",
"manifest_version": 3,
"icons": {
"48": "icons/icon.png"
},
"host_permissions": ["https://*/"],
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": [
"scripting",
"contextMenus",
"activeTab",
"webNavigation",
"nativeMessaging"
],
"action": {
"default_popup": "popup.html"
}
}

13
extension/popup.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Popup</title>
</head>
<body>
<h1>test</h1>
<script src="popup.js"></script>
</body>
</html>

1
extension/popup.js Normal file
View File

@ -0,0 +1 @@
// chrome.tabs.sendMessage(tabId, "message", function (response));

View File

@ -0,0 +1,7 @@
{
"name": "ee.lunasqu.password_manager",
"description": "Password manager native messager",
"path": "server.js",
"type": "stdio",
"allowed_origins": [ "chrome-extension://hafjmpnminafbnikdojlbafngmeoplik/" ]
}

24
host/package-lock.json generated Normal file
View File

@ -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=="
}
}
}

5
host/package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"chrome-native-messaging": "^0.2.0"
}
}

79
host/server.js Executable file
View File

@ -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);