diff --git a/add.html b/add.html
new file mode 100644
index 0000000..f466302
--- /dev/null
+++ b/add.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/add.js b/add.js
new file mode 100644
index 0000000..344bcd1
--- /dev/null
+++ b/add.js
@@ -0,0 +1,126 @@
+/* eslint-disable no-console */
+/* global browser Notify storePathComponents */
+
+const notify = new Notify(document.querySelector('#notify'));
+
+async function mainLoaded() {
+ const tabs = await browser.tabs.query({
+ active: true,
+ currentWindow: true
+ });
+ for (let tabIndex = 0; tabIndex < tabs.length; tabIndex++) {
+ const tab = tabs[tabIndex];
+ if (tab.url) {
+ currentTabId = tab.id;
+ currentUrl = tab.url;
+ break;
+ }
+ }
+
+ document.getElementById('addButton').addEventListener('click', addButtonClick, false);
+ document.getElementById('showPasswordButton').addEventListener('click', showPasswordClick, false);
+
+ const vaultServer = document.getElementById('urlBox');
+ vaultServer.value = new URL(currentUrl).host;
+
+ try {
+ await populateDirectorySelection();
+ } catch (err) {
+ notify.clear().error(err.message);
+ return;
+ }
+
+ let secretList = (await browser.storage.sync.get('secrets')).secrets || [];
+ if (secretList) {
+ try {
+ await querySecretsCallback(currentUrl, secretList[0], function(element, credentialsSets) {
+ if (credentialsSets) {
+ const c = credentialsSets[0];
+ document.getElementById('urlBox').value = element;
+ }
+ });
+ } catch (err) {
+ notify.clear().error(err.message);
+ return
+ }
+ }
+}
+
+/**
+ * populate the choose list of secrets directories when adding
+ */
+async function populateDirectorySelection(vaultServerAddress, vaultToken, policies, storePath) {
+ const fetchListOfSecretDirs = await vaultApiCall('LIST', 'metadata', '', 'Fetching secrets directories');
+
+ let activeSecrets = (await browser.storage.sync.get('secrets')).secrets || [];
+ const availableSecrets = (await fetchListOfSecretDirs.json()).data.keys;
+ activeSecrets = activeSecrets.filter((x) => availableSecrets.indexOf(x) !== -1);
+
+ const dirsList = document.getElementById('dirsList');
+ var first = 1;
+ for (const secret of activeSecrets) {
+ var option = document.createElement('option');
+ option.value = secret;
+ if (first) {
+ first = 0;
+ option.selected = true;
+ const dirBox = document.getElementById('dirBox')
+ dirBox.placeholder = secret;
+ dirBox.value = secret;
+ }
+ dirsList.appendChild(option);
+ }
+}
+
+async function addButtonClick() {
+ const dirBox = document.getElementById('dirBox').value;
+ const urlBox = document.getElementById('urlBox').value;
+ const loginBox = document.getElementById('loginBox').value;
+ const passBox = document.getElementById('passBox').value;
+ // verify input not empty. TODO: verify correct URL format.
+ if (urlBox.includes("/")) {
+ notify.error("Bad input, url has slash")
+ return
+ }
+ if (dirBox.length == 0 || urlBox.length == 0 || loginBox.length == 0 ||
+ passBox.length == 0) {
+ notify.error("Bad input, field is empty")
+ return
+ }
+ // get current value if exists
+ const passpath = dirBox + urlBox;
+ const resp = await vaultApiCall("GET", 'data', passpath, '')
+ const respjson = resp.ok ? await resp.json() : {};
+ const data = resp.ok ? respjson.data : {};
+ const cas = resp.ok ? respjson.data.metadata.version : 0;
+ const cur = resp.ok ? respjson.data.data : {};
+ const userkey = `username-vaultpass-${loginBox}`;
+ cur[userkey] = loginBox;
+ cur[`password-vaultpass-${passBox}`] = passBox;
+ const postdata = {
+ 'data': cur,
+ 'options': {
+ 'cas': cas,
+ },
+ };
+ const postdatajson = JSON.stringify(postdata);
+ //notify.error(`cur=${cur} cas=${cas} data=${postdatajson}`);
+ const resp2 =
+ await vaultApiCall("POST", 'data', passpath,
+ `could not update value with ${postdatajson}`, postdata);
+
+ document.getElementById('loginBox').value = "";
+ document.getElementById('passBox').value = "";
+ notify.success(`Added entry ${userkey} to ${passpath}`);
+}
+
+function showPasswordClick() {
+ var x = document.getElementById("passBox");
+ if (x.type === "password") {
+ x.type = "text";
+ } else {
+ x.type = "password";
+ }
+}
+
+document.addEventListener('DOMContentLoaded', mainLoaded, false);
diff --git a/common.js b/common.js
index 3d49fc3..1c2da4a 100644
--- a/common.js
+++ b/common.js
@@ -3,21 +3,119 @@
/* global browser chrome */
function storePathComponents(storePath) {
- let path = 'secret/vaultPass';
- if (storePath && storePath.length > 0) {
- path = storePath;
- }
+ const path = storePath && storePath.length > 0 ? storePath : 'secret/vaultPass';
const pathComponents = path.split('/');
const storeRoot = pathComponents[0];
- const storeSubPath =
- pathComponents.length > 0 ? pathComponents.slice(1).join('/') : '';
-
+ const storeSubPath = pathComponents.length > 0 ? pathComponents.slice(1).join('/') : '';
return {
root: storeRoot,
- subPath: storeSubPath,
+ subPath: storeSubPath ? '/' + storeSubPath : '',
};
}
+/**
+ * Make a call to vault api.
+ * @param string method GET or POST or LIST etc.
+ * @param midpath The middle of the vault path. Basically "metadata" or "data".
+ * @param path The suffix of the path to query from vault.
+ * @param string error if set, will error this if not ok.
+ * @param dict body if set, will add it to POST it
+ */
+async function vaultApiCall(method, midpath, path = "", error = "", body = undefined) {
+ const vaultToken = (await browser.storage.local.get('vaultToken')).vaultToken;
+ const vaultServerAddress = (await browser.storage.sync.get('vaultAddress')).vaultAddress;
+ const storePath = (await browser.storage.sync.get('storePath')).storePath;
+ const storeComponents = storePathComponents(storePath);
+ if (path) {
+ // make sure path has leading slash.
+ path = "/" + path.replace(/^\/*/, "");
+ }
+ const url = `${vaultServerAddress}/v1/${storeComponents.root}/${midpath}${storeComponents.subPath}${path}`;
+ const res = await fetch(url, {
+ method: method,
+ headers: {
+ 'X-Vault-Token': vaultToken,
+ 'Content-Type': 'application/json',
+ },
+ body: body === undefined ? undefined : JSON.stringify(body),
+ });
+ if (error && (!res.ok || res.status != 200)) {
+ const apiResponse = await res.json();
+ const msg = `ERROR: ${error}. Calling ${url} failed with status=${
+ res.status}. ${apiResponse.errors.join('. ')}`
+ notify.error(msg);
+ throw msg;
+ }
+ return res;
+}
+
+/**
+ * From data returned from vault in data extract the credentials.
+ */
+function extractCredentialsSets(data) {
+ const keys = Object.keys(data);
+ const credentials = [];
+ for (const key of keys) {
+ if (key.startsWith('username')) {
+ const suffix = key.substring(8);
+ const passwordField = 'password' + suffix;
+ if (data[passwordField]) {
+ credentials.push({
+ username: data[key],
+ password: data['password' + suffix],
+ title: data.hasOwnProperty('title' + suffix)
+ ? data['title' + suffix]
+ : data.hasOwnProperty('title')
+ ? data['title']
+ : '',
+ comment: data.hasOwnProperty('comment' + suffix)
+ ? data['comment' + suffix]
+ : data.hasOwnProperty('comment')
+ ? data['comment']
+ : '',
+ });
+ }
+ }
+ }
+ return credentials;
+}
+
+/**
+ * small wrapper around vault api call to get the secrets stored in kv in vault at urlpath
+ */
+async function getCredentials(urlPath) {
+ const result = await vaultApiCall("GET", "data", urlPath, "getting credentials")
+ return await result.json();
+}
+
+/**
+ * makes a query to get all secrets
+ * @param string searchString The saerched string, most probably URL.
+ * @param string secret
+ * @param function(string, credentials[]) callback Callback to call with the url and credentials.
+ */
+async function querySecretsCallback(searchString, secret, callback) {
+ const secretsInPath = await vaultApiCall("LIST", "metadata", `${secret}`)
+ if (!secretsInPath.ok) {
+ if (secretsInPath.status !== 404) {
+ notify.error(`Unable to read ${secret}... Try re-login`, {
+ removeOption: true,
+ });
+ }
+ return;
+ }
+ for (const element of (await secretsInPath.json()).data.keys) {
+ const pattern = new RegExp(element);
+ const patternMatches = pattern.test(searchString) || element.includes(searchString);
+ if (patternMatches) {
+ const credentials = await getCredentials(`${secret}${element}`);
+ const credentialsSets = extractCredentialsSets(credentials.data.data);
+ callback(element, credentialsSets);
+ notify.clear();
+ }
+ }
+}
+
if (!browser.browserAction) {
browser.browserAction = chrome.browserAction ?? chrome.action;
}
diff --git a/options.html b/options.html
index 4180fc5..999ea60 100644
--- a/options.html
+++ b/options.html
@@ -17,14 +17,19 @@