diff --git a/add.html b/add.html new file mode 100644 index 0000000..f466302 --- /dev/null +++ b/add.html @@ -0,0 +1,75 @@ + + + + + + Vault for Chrome: Add secret + + + + + +
+
+

VaultPass

+ +
+
+ +
+ +
+ + + + + + + +
+
+ + + + + + 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 @@

VaultPass

diff --git a/options.js b/options.js index d8dabf2..2c502e6 100644 --- a/options.js +++ b/options.js @@ -67,37 +67,13 @@ async function querySecrets( }); } - const storeComponents = storePathComponents(storePath); + const fetchListOfSecretDirs = await vaultApiCall('LIST', 'metadata', '', + `Fetching secrets directories at "${storePath}" failed`); - const fetchListOfSecretDirs = await fetch( - `${vaultServerAddress}/v1/${storeComponents.root}/metadata/${storeComponents.subPath}`, - { - method: 'LIST', - headers: { - 'X-Vault-Token': vaultToken, - 'Content-Type': 'application/json', - }, - } - ); - if (!fetchListOfSecretDirs.ok) { - const apiResponse = await fetchListOfSecretDirs.json(); - notify.error( - `Fetching secrets directories at "${storePath}" failed. ${apiResponse.errors.join( - '. ' - )}` - ); - return; - } - - let activeSecrets = (await browser.storage.sync.get('secrets')).secrets; - if (!activeSecrets) { - activeSecrets = []; - } + let activeSecrets = (await browser.storage.sync.get('secrets')).secrets || []; const availableSecrets = (await fetchListOfSecretDirs.json()).data.keys; - activeSecrets = activeSecrets.filter( - (x) => availableSecrets.indexOf(x) !== -1 - ); + activeSecrets = activeSecrets.filter((x) => availableSecrets.indexOf(x) !== -1); await browser.storage.sync.set({ secrets: activeSecrets }); await displaySecrets(availableSecrets, activeSecrets); } @@ -164,32 +140,14 @@ async function displaySecrets(secrets, activeSecrets) { } async function secretChanged({ checkbox, item }) { - let activeSecrets = (await browser.storage.sync.get('secrets')).secrets; - if (!activeSecrets) { - activeSecrets = []; - } + let activeSecrets = (await browser.storage.sync.get('secrets')).secrets || []; if (checkbox.checked) { - const vaultServerAddress = (await browser.storage.sync.get('vaultAddress')) - .vaultAddress; - const vaultToken = (await browser.storage.local.get('vaultToken')) - .vaultToken; + const vaultToken = (await browser.storage.local.get('vaultToken')).vaultToken; if (!vaultToken) { throw new Error('secretChanged: Vault Token is empty after login'); } - - const storePath = (await browser.storage.sync.get('storePath')).storePath; - const storeComponents = storePathComponents(storePath); - const fetchListOfSecretsForDir = await fetch( - `${vaultServerAddress}/v1/${storeComponents.root}/metadata/${storeComponents.subPath}/${checkbox.name}`, - { - method: 'LIST', - headers: { - 'X-Vault-Token': vaultToken, - 'Content-Type': 'application/json', - }, - } - ); + const fetchListOfSecretsForDir = await vaultApiCall('LIST', 'metadata', checkbox.name); if (!fetchListOfSecretsForDir.ok) { checkbox.checked = false; checkbox.disabled = true; diff --git a/popup.html b/popup.html index 491984e..e3ba09d 100644 --- a/popup.html +++ b/popup.html @@ -17,14 +17,20 @@

VaultPass

diff --git a/popup.js b/popup.js index f83ae02..72d6623 100644 --- a/popup.js +++ b/popup.js @@ -6,7 +6,7 @@ const notify = new Notify(document.querySelector('#notify')); const resultList = document.getElementById('resultList'); const searchInput = document.getElementById('vault-search'); var currentUrl, currentTabId; -var vaultServerAddress, vaultToken, storePath, secretList; +var vaultServerAddress, vaultToken; async function mainLoaded() { const tabs = await browser.tabs.query({ active: true, currentWindow: true }); @@ -32,15 +32,8 @@ async function mainLoaded() { ); } - vaultServerAddress = (await browser.storage.sync.get('vaultAddress')) - .vaultAddress; + vaultServerAddress = (await browser.storage.sync.get('vaultAddress')).vaultAddress; - storePath = (await browser.storage.sync.get('storePath')).storePath; - - secretList = (await browser.storage.sync.get('secrets')).secrets; - if (!secretList) { - secretList = []; - } await querySecrets(currentUrl, searchInput.value.length !== 0); } @@ -53,51 +46,18 @@ async function querySecrets(searchString, manualSearch) { const promises = []; notify.clear(); - const storeComponents = storePathComponents(storePath); let matches = 0; + let secretList = (await browser.storage.sync.get('secrets')).secrets || []; for (const secret of secretList) { - promises.push( - (async function () { - const secretsInPath = await fetch( - `${vaultServerAddress}/v1/${storeComponents.root}/metadata/${storeComponents.subPath}/${secret}`, - { - method: 'LIST', - headers: { - 'X-Vault-Token': vaultToken, - 'Content-Type': 'application/json', - }, - } - ); - 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 urlPath = `${vaultServerAddress}/v1/${storeComponents.root}/data/${storeComponents.subPath}/${secret}${element}`; - const credentials = await getCredentials(urlPath); - const credentialsSets = extractCredentialsSets( - credentials.data.data - ); - - for (const item of credentialsSets) { - addCredentialsToList(item, element, resultList); - - matches++; - } - - notify.clear(); + promises.push(querySecretsCallback(searchString, secret, + function(element, credentialsSets) { + for (const item of credentialsSets) { + addCredentialsToList(item, element, resultList); + matches++; } } - })() + ) ); } @@ -135,35 +95,6 @@ const searchHandler = function (e) { searchInput.addEventListener('keyup', searchHandler); -function extractCredentialsSets(data) { - const keys = Object.keys(data); - const credentials = []; - - for (const key of keys) { - if (key.startsWith('username')) { - const passwordField = 'password' + key.substring(8); - if (data[passwordField]) { - credentials.push({ - username: data[key], - password: data['password' + key.substring(8)], - title: data.hasOwnProperty('title' + key.substring(8)) - ? data['title' + key.substring(8)] - : data.hasOwnProperty('title') - ? data['title'] - : '', - comment: data.hasOwnProperty('comment' + key.substring(8)) - ? data['comment' + key.substring(8)] - : data.hasOwnProperty('comment') - ? data['comment'] - : '', - }); - } - } - } - - return credentials; -} - function addCredentialsToList(credentials, credentialName, list) { const item = document.createElement('li'); item.classList.add('list__item', 'list__item--three-line'); @@ -228,20 +159,6 @@ function addCredentialsToList(credentials, credentialName, list) { list.appendChild(item); } -async function getCredentials(urlPath) { - const vaultToken = (await browser.storage.local.get('vaultToken')).vaultToken; - const result = await fetch(urlPath, { - headers: { - 'X-Vault-Token': vaultToken, - 'Content-Type': 'application/json', - }, - }); - if (!result.ok) { - throw new Error(`getCredentials: ${await result.text}`); - } - return await result.json(); -} - async function fillCredentialsInBrowser(username, password) { const tabs = await browser.tabs.query({ active: true, currentWindow: true }); for (let tabIndex = 0; tabIndex < tabs.length; tabIndex++) {