Skip to content
This repository was archived by the owner on May 27, 2019. It is now read-only.

Allow configuring multiple custom password stores #237

Merged
merged 38 commits into from
Mar 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9f89770
Allow using multiple password stores
maximbaz Mar 24, 2018
ce8e255
Add selector for custom password stores to options
maximbaz Mar 24, 2018
71bc2e2
Send settings to the host app with every call
maximbaz Mar 24, 2018
66a5f81
Small style improvements on options dialog
maximbaz Mar 24, 2018
408b1a3
Fix placeholder value
maximbaz Mar 24, 2018
f4d18df
Switch options.js to browserify generation
erayd Mar 26, 2018
34af039
Rewrite options screen
erayd Mar 26, 2018
16b261d
Properly declare local variables as local
erayd Mar 27, 2018
a665883
Add some spacing
erayd Mar 27, 2018
38799f5
Use getSettings() instead of directly reading from local storage
erayd Mar 27, 2018
6259c7c
Define getSettings in a more general manner
erayd Mar 27, 2018
6d6508a
Use a list of named password stores
erayd Mar 27, 2018
049494d
Multi-store display mode
erayd Mar 27, 2018
d12a948
Don't display the store badge if the store name isn't present
erayd Mar 27, 2018
1b6df1c
Increase the minimum width of the popup by 70px
erayd Mar 27, 2018
ca7e084
Firefox UI improvements
erayd Mar 27, 2018
6ac632f
Return some error messages from the native host app
erayd Mar 27, 2018
86a7ded
Don't search empty or ignored domains
erayd Mar 27, 2018
696ff4a
Use settings object from search
erayd Mar 27, 2018
c85ad59
Don't show badges if there is only one custom store configured
erayd Mar 27, 2018
173e554
Remove unused variable
erayd Mar 27, 2018
8c8ca8f
Remove prefix when detecting the URL from the path
erayd Mar 27, 2018
abe6a7c
Fix hints
erayd Mar 27, 2018
af8df7e
Remove the fish
erayd Mar 27, 2018
c1d750a
Remove TODO comment
erayd Mar 27, 2018
6e9e96f
Remove TODO comment
erayd Mar 27, 2018
d1178ed
Use plural name for variable
erayd Mar 27, 2018
f686716
Add default empty entry
erayd Mar 27, 2018
a9aa62c
Merge pull request #239 from erayd/multiple-configurable-password-stores
maximbaz Mar 27, 2018
2dea7c0
Resolve conflict with master
maximbaz Mar 27, 2018
a319bda
Merge branch 'master' into multiple-configurable-password-stores
maximbaz Mar 27, 2018
31b2a0c
Allow using environment vars and tilde in custom stores
maximbaz Mar 27, 2018
a9a4617
Run prettier
maximbaz Mar 27, 2018
c361f7b
Make fuzzy search ignore store name, fix tests
maximbaz Mar 27, 2018
6d8e5ee
Merge remote-tracking branch 'origin/master' into multiple-configurab…
maximbaz Mar 27, 2018
af637e6
Add unit tests for multiple stores
maximbaz Mar 27, 2018
53e1cf0
Add tests goal to the makefile
maximbaz Mar 27, 2018
f5b3cf9
I don't follow the math, but this looks better in Firefox
maximbaz Mar 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ firefox/*
!firefox/host.json
!firefox/manifest.json
chrome/script.js
chrome/options.js
node_modules
.vagrant/
vendor/
Expand Down
46 changes: 31 additions & 15 deletions browserpass.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ var endianness = binary.LittleEndian
// The browser extension will look up settings in its localstorage and find
// which options have been selected by the user, and put them in a JSON object
// which is then passed along with the command over the native messaging api.

// Config defines the root config structure sent from the browser extension
type Config struct {
// Manual searches use FuzzySearch if true, GlobSearch otherwise
UseFuzzy bool `json:"use_fuzzy_search"`
UseFuzzy bool `json:"use_fuzzy_search"`
CustomStores []pass.StoreDefinition `json:"customStores"`
}

// msg defines a message sent from a browser extension.
Expand All @@ -46,64 +49,77 @@ type msg struct {
Entry string `json:"entry"`
}

func SendError(err error, stdout io.Writer) error {
var buf bytes.Buffer
if writeError := json.NewEncoder(&buf).Encode(err.Error()); writeError != nil {
return err
}
if writeError := binary.Write(stdout, endianness, uint32(buf.Len())); writeError != nil {
return err
}
buf.WriteTo(stdout)
return err
}

// Run starts browserpass.
func Run(stdin io.Reader, stdout io.Writer, s pass.Store) error {
func Run(stdin io.Reader, stdout io.Writer) error {
protector.Protect("stdio rpath proc exec")
for {
// Get message length, 4 bytes
var n uint32
if err := binary.Read(stdin, endianness, &n); err == io.EOF {
return nil
} else if err != nil {
return err
return SendError(err, stdout)
}

// Get message body
var data msg
lr := &io.LimitedReader{R: stdin, N: int64(n)}
if err := json.NewDecoder(lr).Decode(&data); err != nil {
return err
return SendError(err, stdout)
}

// Since the pass.Store object is created by the wrapper prior to
// settings from the browser being made available, we set them here
s.SetConfig(&data.Settings.UseFuzzy)
s, err := pass.NewDefaultStore(data.Settings.CustomStores, data.Settings.UseFuzzy)
if err != nil {
return SendError(err, stdout)
}

var resp interface{}
switch data.Action {
case "search":
list, err := s.Search(data.Domain)
if err != nil {
return err
return SendError(err, stdout)
}
resp = list
case "match_domain":
list, err := s.GlobSearch(data.Domain)
if err != nil {
return err
return SendError(err, stdout)
}
resp = list
case "get":
rc, err := s.Open(data.Entry)
if err != nil {
return err
return SendError(err, stdout)
}
defer rc.Close()
login, err := readLoginGPG(rc)
if err != nil {
return err
return SendError(err, stdout)
}
if login.Username == "" {
login.Username = guessUsername(data.Entry)
}
resp = login
default:
return errors.New("Invalid action")
return SendError(errors.New("Invalid action"), stdout)
}

var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(resp); err != nil {
return err
return SendError(err, stdout)
}

if err := binary.Write(stdout, endianness, uint32(b.Len())); err != nil {
Expand Down Expand Up @@ -185,7 +201,7 @@ func parseTotp(str string, l *Login) error {

if ourl == "" {
tokenPattern := regexp.MustCompile("(?i)^totp(-secret)?:")
token := tokenPattern.ReplaceAllString(str, "");
token := tokenPattern.ReplaceAllString(str, "")
if len(token) != len(str) {
ourl = "otpauth://totp/?secret=" + strings.TrimSpace(token)
}
Expand Down Expand Up @@ -222,7 +238,7 @@ func parseLogin(r io.Reader) (*Login, error) {
if len(replaced) != len(line) {
login.Username = strings.TrimSpace(replaced)
}
if (login.URL == "") {
if login.URL == "" {
replaced = urlPattern.ReplaceAllString(line, "")
if len(replaced) != len(line) {
login.URL = strings.TrimSpace(replaced)
Expand Down
43 changes: 32 additions & 11 deletions chrome/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ chrome.runtime.onInstalled.addListener(onExtensionInstalled);
// fill login form & submit
function fillLoginForm(login, tab) {
const loginParam = JSON.stringify(login);
const autoSubmit = localStorage.getItem("autoSubmit");
const autoSubmitParam = autoSubmit == "true";
if (autoSubmit === null) {
localStorage.setItem("autoSubmit", autoSubmitParam);
}

chrome.tabs.executeScript(
tab.id,
{
Expand All @@ -27,7 +21,7 @@ function fillLoginForm(login, tab) {
function() {
chrome.tabs.executeScript({
allFrames: true,
code: `browserpassFillForm(${loginParam}, ${autoSubmitParam});`
code: `browserpassFillForm(${loginParam}, ${getSettings().autoSubmit});`
});
}
);
Expand Down Expand Up @@ -59,7 +53,7 @@ function onMessage(request, sender, sendResponse) {
if (request.action == "login") {
chrome.runtime.sendNativeMessage(
app,
{ action: "get", entry: request.entry },
{ action: "get", entry: request.entry, settings: getSettings() },
function(response) {
if (chrome.runtime.lastError) {
var error = chrome.runtime.lastError.message;
Expand Down Expand Up @@ -92,9 +86,7 @@ function onMessage(request, sender, sendResponse) {
// object that has current settings. Update this as new settings
// are added (or old ones removed)
if (request.action == "getSettings") {
const use_fuzzy_search =
localStorage.getItem("use_fuzzy_search") != "false";
sendResponse({ use_fuzzy_search: use_fuzzy_search });
sendResponse(getSettings());
}

// spawn a new tab with pre-provided credentials
Expand All @@ -121,6 +113,35 @@ function onMessage(request, sender, sendResponse) {
}
}

function getSettings() {
// default settings
var settings = {
autoSubmit: false,
use_fuzzy_search: true,
customStores: []
};

// load settings from local storage
for (var key in settings) {
var value = localStorage.getItem(key);
if (value !== null) {
settings[key] = JSON.parse(value);
}
}

// filter custom stores by enabled & path length, and ensure they are named
settings.customStores = settings.customStores
.filter(store => store.enabled && store.path.length > 0)
.map(function(store) {
if (!store.name) {
store.name = store.path;
}
return store;
});

return settings;
}

// listener function for authentication interception
function onAuthRequired(request, requestDetails) {
// ask the user before sending credentials to a different domain
Expand Down
134 changes: 134 additions & 0 deletions chrome/options.browserify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
var settings = {
autoSubmit: {
type: "checkbox",
title: "Automatically submit forms after filling",
value: false
},
use_fuzzy_search: {
type: "checkbox",
title: "Use fuzzy search",
value: true
},
customStores: {
title: "Custom password store locations",
value: [{ enabled: true, name: "", path: "" }]
}
};

// load settings & create render tree
loadSettings();
var tree = {
view: function() {
var nodes = [m("h3", "Basic Settings")];
for (var key in settings) {
var type = settings[key].type;
if (type == "checkbox") {
nodes.push(createCheckbox(key, settings[key]));
}
}
nodes.push(m("h3", "Custom Store Locations"));
for (var key in settings.customStores.value) {
nodes.push(createCustomStore(key, settings.customStores.value[key]));
}
nodes.push(
m(
"button.add-store",
{
onclick: function() {
settings.customStores.value.push({
enabled: true,
name: "",
path: ""
});
saveSetting("customStores");
}
},
"Add Store"
)
);
return nodes;
}
};

// attach tree
var m = require("mithril");
m.mount(document.body, tree);

// load settings from local storage
function loadSettings() {
for (var key in settings) {
var value = localStorage.getItem(key);
if (value !== null) {
settings[key].value = JSON.parse(value);
}
}
}

// save settings to local storage
function saveSetting(name) {
var value = settings[name].value;
if (Array.isArray(value)) {
value = value.filter(item => item !== null);
}
value = JSON.stringify(value);
localStorage.setItem(name, value);
}

// create a checkbox option
function createCheckbox(name, option) {
return m("div.option", { class: name }, [
m("input[type=checkbox]", {
name: name,
title: option.title,
checked: option.value,
onchange: function(e) {
settings[name].value = e.target.checked;
saveSetting(name);
}
}),
m("label", { for: name }, option.title)
]);
}

// create a custom store option
function createCustomStore(key, store) {
return m("div.option.custom-store", { class: "store-" + store.name }, [
m("input[type=checkbox]", {
title: "Whether to enable this password store",
checked: store.enabled,
onchange: function(e) {
store.enabled = e.target.checked;
saveSetting("customStores");
}
}),
m("input[type=text].name", {
title: "The name for this password store",
value: store.name,
placeholder: "name",
onchange: function(e) {
store.name = e.target.value;
saveSetting("customStores");
}
}),
m("input[type=text].path", {
title: "The full path to this password store",
value: store.path,
placeholder: "/path/to/store",
onchange: function(e) {
store.path = e.target.value;
saveSetting("customStores");
}
}),
m(
"a.remove",
{
title: "Remove this password store",
onclick: function() {
delete settings.customStores.value[key];
saveSetting("customStores");
}
},
"[X]"
)
]);
}
Loading