initial commit
This commit is contained in:
commit
e227f9aaf7
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
169
extension/background.js
Normal file
169
extension/background.js
Normal 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
229
extension/foreground.js
Normal 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
BIN
extension/icons/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
28
extension/manifest.json
Normal file
28
extension/manifest.json
Normal 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
13
extension/popup.html
Normal 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
1
extension/popup.js
Normal file
@ -0,0 +1 @@
|
||||
// chrome.tabs.sendMessage(tabId, "message", function (response));
|
7
host/ee.lunasqu.password_manager.json
Normal file
7
host/ee.lunasqu.password_manager.json
Normal 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
24
host/package-lock.json
generated
Normal 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
5
host/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"chrome-native-messaging": "^0.2.0"
|
||||
}
|
||||
}
|
79
host/server.js
Executable file
79
host/server.js
Executable 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);
|
Loading…
Reference in New Issue
Block a user