From 9f897707d8c6a24dc6279a8f7e0316a3b6f08a7d Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Sat, 24 Mar 2018 21:26:05 +0100 Subject: [PATCH 01/35] Allow using multiple password stores Known limitation: two password stores cannot have identical file names --- browserpass.go | 16 +++--- cmd/browserpass/main.go | 8 +-- pass/disk.go | 116 ++++++++++++++++++++++------------------ pass/disk_test.go | 63 ++++++++++++---------- pass/pass.go | 6 --- 5 files changed, 107 insertions(+), 102 deletions(-) diff --git a/browserpass.go b/browserpass.go index 93a155e..b9b5f64 100644 --- a/browserpass.go +++ b/browserpass.go @@ -35,7 +35,8 @@ var endianness = binary.LittleEndian // which is then passed along with the command over the native messaging api. type Config struct { // Manual searches use FuzzySearch if true, GlobSearch otherwise - UseFuzzy bool `json:"use_fuzzy_search"` + UseFuzzy bool `json:"use_fuzzy_search"` + Directories []string `json:"directories"` } // msg defines a message sent from a browser extension. @@ -47,7 +48,7 @@ type msg struct { } // 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 @@ -65,9 +66,10 @@ func Run(stdin io.Reader, stdout io.Writer, s pass.Store) error { return err } - // 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.Directories, data.Settings.UseFuzzy) + if err != nil { + return err + } var resp interface{} switch data.Action { @@ -185,7 +187,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) } @@ -222,7 +224,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) diff --git a/cmd/browserpass/main.go b/cmd/browserpass/main.go index aae811a..0345e5e 100644 --- a/cmd/browserpass/main.go +++ b/cmd/browserpass/main.go @@ -6,7 +6,6 @@ import ( "os" "github.com/dannyvankooten/browserpass" - "github.com/dannyvankooten/browserpass/pass" "github.com/dannyvankooten/browserpass/protector" ) @@ -14,12 +13,7 @@ func main() { protector.Protect("stdio rpath proc exec getpw") log.SetPrefix("[Browserpass] ") - s, err := pass.NewDefaultStore() - if err != nil { - log.Fatal(err) - } - - if err := browserpass.Run(os.Stdin, os.Stdout, s); err != nil && err != io.EOF { + if err := browserpass.Run(os.Stdin, os.Stdout); err != nil && err != io.EOF { log.Fatal(err) } } diff --git a/pass/disk.go b/pass/disk.go index cbd7f94..39a9761 100644 --- a/pass/disk.go +++ b/pass/disk.go @@ -15,52 +15,54 @@ import ( ) type diskStore struct { - path string + paths []string useFuzzy bool // Setting for FuzzySearch or GlobSearch in manual searches } -func NewDefaultStore() (Store, error) { - path, err := defaultStorePath() - if err != nil { - return nil, err +func NewDefaultStore(paths []string, useFuzzy bool) (Store, error) { + if paths == nil || len(paths) == 0 { + defaultPaths, err := defaultStorePath() + if err != nil { + return nil, err + } + paths = defaultPaths } - return &diskStore{path, false}, nil -} - -func defaultStorePath() (string, error) { - usr, err := user.Current() - - if err != nil { - return "", err + // Follow symlinks + finalPaths := make([]string, len(paths)) + for i, path := range paths { + finalPath, err := filepath.EvalSymlinks(path) + if err != nil { + return nil, err + } + finalPaths[i] = finalPath } + return &diskStore{finalPaths, useFuzzy}, nil +} + +func defaultStorePath() ([]string, error) { path := os.Getenv("PASSWORD_STORE_DIR") - if path == "" { - path = filepath.Join(usr.HomeDir, ".password-store") + if path != "" { + return []string{path}, nil } - // Follow symlinks - return filepath.EvalSymlinks(path) -} - -// Set the configuration options for password matching. -func (s *diskStore) SetConfig(use_fuzzy *bool) error { - if use_fuzzy != nil { - s.useFuzzy = *use_fuzzy + usr, err := user.Current() + if err != nil { + return nil, err } - return nil + + path = filepath.Join(usr.HomeDir, ".password-store") + return []string{path}, nil } // Do a search. Will call into the correct algoritm (glob or fuzzy) // based on the settings present in the diskStore struct func (s *diskStore) Search(query string) ([]string, error) { - // default glob search - if !s.useFuzzy { - return s.GlobSearch(query) - } else { + if s.useFuzzy { return s.FuzzySearch(query) } + return s.GlobSearch(query) } // Fuzzy searches first get a list of all pass entries by doing a glob search @@ -68,7 +70,7 @@ func (s *diskStore) Search(query string) ([]string, error) { // a slice of strings, finally returning all of the unique entries. func (s *diskStore) FuzzySearch(query string) ([]string, error) { var items []string - file_list, err := s.GlobSearch("") + fileList, err := s.GlobSearch("") if err != nil { return nil, err } @@ -76,9 +78,9 @@ func (s *diskStore) FuzzySearch(query string) ([]string, error) { // The resulting match struct does not copy the strings, but rather // provides the index to the original array. Copy those strings // into the result slice - matches := sfuzzy.Find(query, file_list) + matches := sfuzzy.Find(query, fileList) for _, match := range matches { - items = append(items, file_list[match.Index]) + items = append(items, fileList[match.Index]) } return items, nil @@ -90,24 +92,33 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { // 2. DOMAIN.gpg // 3. DOMAIN/SUBDIRECTORY/USERNAME.gpg - matches, err := zglob.GlobFollowSymlinks(s.path + "/**/" + query + "*/**/*.gpg") - if err != nil { - return nil, err - } + items := []string{} - matches2, err := zglob.GlobFollowSymlinks(s.path + "/**/" + query + "*.gpg") - if err != nil { - return nil, err - } + for _, path := range s.paths { + matches, err := zglob.GlobFollowSymlinks(path + "/**/" + query + "*/**/*.gpg") + if err != nil { + return nil, err + } - items := append(matches, matches2...) - for i, path := range items { - item, err := filepath.Rel(s.path, path) + matches2, err := zglob.GlobFollowSymlinks(path + "/**/" + query + "*.gpg") if err != nil { return nil, err } - items[i] = strings.TrimSuffix(item, ".gpg") + + allMatches := append(matches, matches2...) + + for i, match := range allMatches { + // TODO this does not handle identical file names in multiple s.paths + item, err := filepath.Rel(path, match) + if err != nil { + return nil, err + } + allMatches[i] = strings.TrimSuffix(item, ".gpg") + } + + items = append(items, allMatches...) } + if strings.Count(query, ".") >= 2 { // try finding additional items by removing subparts of the query queryParts := strings.SplitN(query, ".", 2)[1:] @@ -125,17 +136,16 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { } func (s *diskStore) Open(item string) (io.ReadCloser, error) { - p := filepath.Join(s.path, item+".gpg") - if !filepath.HasPrefix(p, s.path) { - // Make sure the requested item is *in* the password store - return nil, errors.New("invalid item path") - } - - f, err := os.Open(p) - if os.IsNotExist(err) { - return nil, ErrNotFound + for _, path := range s.paths { + path := filepath.Join(path, item+".gpg") + f, err := os.Open(path) + if os.IsNotExist(err) { + continue + } + // TODO this does not handle identical file names in multiple s.paths + return f, err } - return f, err + return nil, errors.New("Unable to find the item on disk") } func unique(elems []string) []string { diff --git a/pass/disk_test.go b/pass/disk_test.go index 5892e63..7ea51d8 100644 --- a/pass/disk_test.go +++ b/pass/disk_test.go @@ -1,7 +1,6 @@ package pass import ( - "fmt" "os" "os/user" "path/filepath" @@ -9,7 +8,8 @@ import ( ) func TestDefaultStorePath(t *testing.T) { - var home, expected, actual string + var home, expectedCustom string + var expected, actual []string usr, err := user.Current() @@ -21,42 +21,47 @@ func TestDefaultStorePath(t *testing.T) { // default directory os.Setenv("PASSWORD_STORE_DIR", "") - expected = filepath.Join(home, ".password-store") + expected = []string{filepath.Join(home, ".password-store")} actual, _ = defaultStorePath() - if expected != actual { - t.Errorf("1: '%s' does not match '%s'", expected, actual) + + if len(expected) != len(actual) { + t.Errorf("1: '%d' does not match '%d'", len(expected), len(actual)) + } + + if expected[0] != actual[0] { + t.Errorf("2: '%s' does not match '%s'", expected[0], actual[0]) } // custom directory from $PASSWORD_STORE_DIR - expected, err = filepath.Abs("browserpass-test") + expectedCustom, err = filepath.Abs("browserpass-test") if err != nil { t.Error(err) } + expected = []string{expectedCustom} - fmt.Println(expected) - os.Mkdir(expected, os.ModePerm) - os.Setenv("PASSWORD_STORE_DIR", expected) + os.Mkdir(expectedCustom, os.ModePerm) + os.Setenv("PASSWORD_STORE_DIR", expectedCustom) actual, err = defaultStorePath() if err != nil { t.Error(err) } - if expected != actual { - t.Errorf("2: '%s' does not match '%s'", expected, actual) + if len(expected) != len(actual) { + t.Errorf("3: '%d' does not match '%d'", len(expected), len(actual)) + } + if expected[0] != actual[0] { + t.Errorf("4: '%s' does not match '%s'", expected[0], actual[0]) } // clean-up os.Setenv("PASSWORD_STORE_DIR", "") - os.Remove(expected) + os.Remove(expected[0]) } func TestDiskStore_Search_nomatch(t *testing.T) { - s, err := NewDefaultStore() - if err != nil { - t.Fatal(err) - } + store := diskStore{[]string{"test_store"}, false} domain := "this-most-definitely-does-not-exist" - logins, err := s.Search(domain) + logins, err := store.Search(domain) if err != nil { t.Fatal(err) } @@ -66,7 +71,7 @@ func TestDiskStore_Search_nomatch(t *testing.T) { } func TestDiskStoreSearch(t *testing.T) { - store := diskStore{"test_store", false} + store := diskStore{[]string{"test_store"}, false} targetDomain := "abc.com" testDomains := []string{"abc.com", "test.abc.com", "testing.test.abc.com"} for _, domain := range testDomains { @@ -89,7 +94,7 @@ func TestDiskStoreSearch(t *testing.T) { } func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testing.T) { - store := diskStore{"test_store", false} + store := diskStore{[]string{"test_store"}, false} searchResult, err := store.Search("xyz") if err != nil { t.Fatal(err) @@ -104,7 +109,7 @@ func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testi } func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) { - store := diskStore{"test_store", false} + store := diskStore{[]string{"test_store"}, false} searchResult, err := store.Search("def.com") if err != nil { t.Fatal(err) @@ -119,7 +124,7 @@ func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) { } func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) { - store := diskStore{"test_store", false} + store := diskStore{[]string{"test_store"}, false} searchResult, err := store.Search("amazon.co.uk") if err != nil { t.Fatal(err) @@ -134,7 +139,7 @@ func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) { } func TestDiskStoreSearchSubDirectories(t *testing.T) { - store := diskStore{"test_store", false} + store := diskStore{[]string{"test_store"}, false} searchTermsMatches := map[string][]string{ "abc.org": []string{"abc.org/user3", "abc.org/wiki/user4", "abc.org/wiki/work/user5"}, "wiki": []string{"abc.org/wiki/user4", "abc.org/wiki/work/user5"}, @@ -158,7 +163,7 @@ func TestDiskStoreSearchSubDirectories(t *testing.T) { } func TestDiskStorePartSearch(t *testing.T) { - store := diskStore{"test_store", false} + store := diskStore{[]string{"test_store"}, false} searchResult, err := store.Search("ab") if err != nil { t.Fatal(err) @@ -175,14 +180,14 @@ func TestDiskStorePartSearch(t *testing.T) { } func TestFuzzySearch(t *testing.T) { - store := diskStore{"test_store", true} + store := diskStore{[]string{"test_store"}, true} searchResult, err := store.Search("amaz2") if err != nil { t.Fatal(err) } if len(searchResult) != 2 { - t.Fatalf("Result size was: %s expected 2", len(searchResult)) + t.Fatalf("Result size was: %d expected 2", len(searchResult)) } expectedResult := map[string]bool{ @@ -198,26 +203,26 @@ func TestFuzzySearch(t *testing.T) { } func TestFuzzySearchNoResult(t *testing.T) { - store := diskStore{"test_store", true} + store := diskStore{[]string{"test_store"}, true} searchResult, err := store.Search("vvv") if err != nil { t.Fatal(err) } if len(searchResult) != 0 { - t.Fatalf("Result size was: %s expected 0", len(searchResult)) + t.Fatalf("Result size was: %d expected 0", len(searchResult)) } } func TestFuzzySearchTopLevelEntries(t *testing.T) { - store := diskStore{"test_store", true} + store := diskStore{[]string{"test_store"}, true} searchResult, err := store.Search("def") if err != nil { t.Fatal(err) } if len(searchResult) != 1 { - t.Fatalf("Result size was: %s expected 1", len(searchResult)) + t.Fatalf("Result size was: %d expected 1", len(searchResult)) } expectedResult := map[string]bool{ diff --git a/pass/pass.go b/pass/pass.go index d4d5601..b0692d7 100644 --- a/pass/pass.go +++ b/pass/pass.go @@ -2,18 +2,12 @@ package pass import ( - "errors" "io" ) -// ErrNotFound is returned by Store.Open if the requested item is not found. -var ErrNotFound = errors.New("pass: not found") - // Store is a password store. type Store interface { Search(query string) ([]string, error) Open(item string) (io.ReadCloser, error) GlobSearch(query string) ([]string, error) - // Update password store settings on the fly - SetConfig(use_fuzzy *bool) error } From ce8e255f114474b6cac8caf6938da99447b03583 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Sat, 24 Mar 2018 22:09:04 +0100 Subject: [PATCH 02/35] Add selector for custom password stores to options --- chrome/options.html | 31 +++++++++++++++++++++++++++++-- chrome/options.js | 29 +++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/chrome/options.html b/chrome/options.html index a471f5d..824714d 100644 --- a/chrome/options.html +++ b/chrome/options.html @@ -9,16 +9,43 @@ <body> <label> - <input type="checkbox" id="auto-submit"> + <input type="checkbox" id="auto-submit" /> Auto-Submit login forms </label> <br/> +<br/> + <label> - <input type="checkbox" id="use-fuzzy"> + <input type="checkbox" id="use-fuzzy" /> Use fuzzy search </label> <br/> <br/> + +Custom password stores directories: <br/> +<div class="password-store-group"> + <input type="checkbox" class="password-store-path-enabled" /> + <input type="text" class="password-store-path" placeholder="~/.password-store" /> +</div> +<div class="password-store-group"> + <input type="checkbox" class="password-store-path-enabled" /> + <input type="text" class="password-store-path" /> +</div> +<div class="password-store-group"> + <input type="checkbox" class="password-store-path-enabled" /> + <input type="text" class="password-store-path" /> +</div> +<div class="password-store-group"> + <input type="checkbox" class="password-store-path-enabled" /> + <input type="text" class="password-store-path" /> +</div> +<div class="password-store-group"> + <input type="checkbox" class="password-store-path-enabled" /> + <input type="text" class="password-store-path" /> +</div> +<br/> +<br/> + <button id="save">Save</button> <script src="options.js"></script> diff --git a/chrome/options.js b/chrome/options.js index ed027ac..d97db69 100644 --- a/chrome/options.js +++ b/chrome/options.js @@ -1,23 +1,44 @@ function save_options() { - var autoSubmit = document.getElementById("auto-submit").checked; + let autoSubmit = document.getElementById("auto-submit").checked; localStorage.setItem("autoSubmit", autoSubmit); // Options related to fuzzy finding. // use_fuzzy_search indicates if fuzzy finding or glob searching should // be used in manual searches - var use_fuzzy = document.getElementById("use-fuzzy").checked; + let use_fuzzy = document.getElementById("use-fuzzy").checked; localStorage.setItem("use_fuzzy_search", use_fuzzy); + let groups = document.getElementsByClassName("password-store-group"); + let paths = []; + for (let i = 0; i < groups.length; i++) { + let group = groups[i]; + let enabled = group.querySelector(".password-store-path-enabled").checked; + let path = group.querySelector(".password-store-path").value; + if (path) { + paths.push({ path: path, enabled: enabled }); + } + } + localStorage.setItem("paths", JSON.stringify(paths)); + window.close(); } function restore_options() { - var autoSubmit = localStorage.getItem("autoSubmit") == "true"; + let autoSubmit = localStorage.getItem("autoSubmit") == "true"; document.getElementById("auto-submit").checked = autoSubmit; // Restore the view to show the settings described above - var use_fuzzy = localStorage.getItem("use_fuzzy_search") != "false"; + let use_fuzzy = localStorage.getItem("use_fuzzy_search") != "false"; document.getElementById("use-fuzzy").checked = use_fuzzy; + + let groups = document.getElementsByClassName("password-store-group"); + let paths = JSON.parse(localStorage.getItem("paths") || "[]"); + for (let i = 0; i < paths.length; i++) { + let path = paths[i]; + let group = groups[i]; + group.querySelector(".password-store-path-enabled").checked = path.enabled; + group.querySelector(".password-store-path").value = path.path; + } } document.addEventListener("DOMContentLoaded", restore_options); From 71bc2e2570cef22247aa5155f7e8330f2a3dc4d8 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Sat, 24 Mar 2018 22:56:49 +0100 Subject: [PATCH 03/35] Send settings to the host app with every call --- browserpass.go | 6 +- chrome/background.js | 14 ++-- chrome/script.browserify.js | 135 +++++++++++++++++++----------------- 3 files changed, 83 insertions(+), 72 deletions(-) diff --git a/browserpass.go b/browserpass.go index b9b5f64..b44f126 100644 --- a/browserpass.go +++ b/browserpass.go @@ -35,8 +35,8 @@ var endianness = binary.LittleEndian // which is then passed along with the command over the native messaging api. type Config struct { // Manual searches use FuzzySearch if true, GlobSearch otherwise - UseFuzzy bool `json:"use_fuzzy_search"` - Directories []string `json:"directories"` + UseFuzzy bool `json:"use_fuzzy_search"` + Paths []string `json:"paths"` } // msg defines a message sent from a browser extension. @@ -66,7 +66,7 @@ func Run(stdin io.Reader, stdout io.Writer) error { return err } - s, err := pass.NewDefaultStore(data.Settings.Directories, data.Settings.UseFuzzy) + s, err := pass.NewDefaultStore(data.Settings.Paths, data.Settings.UseFuzzy) if err != nil { return err } diff --git a/chrome/background.js b/chrome/background.js index c2bd0ad..c548ca8 100644 --- a/chrome/background.js +++ b/chrome/background.js @@ -59,7 +59,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; @@ -92,9 +92,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 @@ -121,6 +119,14 @@ function onMessage(request, sender, sendResponse) { } } +function getSettings() { + const use_fuzzy_search = localStorage.getItem("use_fuzzy_search") != "false"; + const paths = JSON.parse(localStorage.getItem("paths") || "[]") + .filter(path => path.enabled) + .map(path => path.path); + return { paths: paths, use_fuzzy_search: use_fuzzy_search }; +} + // listener function for authentication interception function onAuthRequired(request, requestDetails) { // ask the user before sending credentials to a different domain diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index 04d0b41..3f9f8ea 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -204,10 +204,10 @@ function searchPassword(_domain, action = "search", useFillOnSubmit = true) { // by requesting them from the background script (which has localStorage access // to the settings). Then construct the message to send to browserpass and // send that via sendNativeMessage. - chrome.runtime.sendMessage({ action: "getSettings" }, function(response) { + chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) { chrome.runtime.sendNativeMessage( app, - { action: action, domain: _domain, settings: response }, + { action: action, domain: _domain, settings: settings }, function(response) { if (chrome.runtime.lastError) { error = chrome.runtime.lastError.message; @@ -250,57 +250,59 @@ function getFaviconUrl(domain) { function launchURL() { var what = this.what; var entry = this.entry; - chrome.runtime.sendNativeMessage( - app, - { action: "get", entry: this.entry }, - function(response) { - if (chrome.runtime.lastError) { - error = chrome.runtime.lastError.message; - m.redraw(); - return; - } - // get url from login path if not available in the host app response - if (!response.hasOwnProperty("url") || response.url.length == 0) { - var parts = entry.split(/\//).reverse(); - for (var i in parts) { - var part = parts[i]; - var info = Tldjs.parse(part); - if ( - info.isValid && - info.tldExists && - info.domain !== null && - info.hostname === part - ) { - response.url = part; - break; + chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) { + chrome.runtime.sendNativeMessage( + app, + { action: "get", entry: entry, settings: settings }, + function(response) { + if (chrome.runtime.lastError) { + error = chrome.runtime.lastError.message; + m.redraw(); + return; + } + // get url from login path if not available in the host app response + if (!response.hasOwnProperty("url") || response.url.length == 0) { + var parts = entry.split(/\//).reverse(); + for (var i in parts) { + var part = parts[i]; + var info = Tldjs.parse(part); + if ( + info.isValid && + info.tldExists && + info.domain !== null && + info.hostname === part + ) { + response.url = part; + break; + } } } + // if a url is present, then launch a new tab via the background script + if (response.hasOwnProperty("url") && response.url.length > 0) { + var url = response.url.match(/^([a-z]+:)?\/\//i) + ? response.url + : "http://" + response.url; + chrome.runtime.sendMessage({ + action: "launch", + url: url, + username: response.u, + password: response.p + }); + window.close(); + return; + } + // no url available + if (!response.hasOwnProperty("url")) { + resetWithError( + "Unable to determine the URL for this entry. If you have defined one in the password file, " + + "your host application must be at least v2.0.14 for this to be usable." + ); + } else { + resetWithError("Unable to determine the URL for this entry."); + } } - // if a url is present, then launch a new tab via the background script - if (response.hasOwnProperty("url") && response.url.length > 0) { - var url = response.url.match(/^([a-z]+:)?\/\//i) - ? response.url - : "http://" + response.url; - chrome.runtime.sendMessage({ - action: "launch", - url: url, - username: response.u, - password: response.p - }); - window.close(); - return; - } - // no url available - if (!response.hasOwnProperty("url")) { - resetWithError( - "Unable to determine the URL for this entry. If you have defined one in the password file, " + - "your host application must be at least v2.0.14 for this to be usable." - ); - } else { - resetWithError("Unable to determine the URL for this entry."); - } - } - ); + ); + }); } function getLoginData() { @@ -326,23 +328,26 @@ function getLoginData() { function loginToClipboard() { var what = this.what; - chrome.runtime.sendNativeMessage( - app, - { action: "get", entry: this.entry }, - function(response) { - if (chrome.runtime.lastError) { - error = chrome.runtime.lastError.message; - m.redraw(); - } else { - if (what === "password") { - toClipboard(response.p); - } else if (what === "username") { - toClipboard(response.u); + var entry = this.entry; + chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) { + chrome.runtime.sendNativeMessage( + app, + { action: "get", entry: entry, settings: settings }, + function(response) { + if (chrome.runtime.lastError) { + error = chrome.runtime.lastError.message; + m.redraw(); + } else { + if (what === "password") { + toClipboard(response.p); + } else if (what === "username") { + toClipboard(response.u); + } + window.close(); } - window.close(); } - } - ); + ); + }); } function toClipboard(s) { From 66a5f81f5175ab2a64b0ec3102caad19d5744be8 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Sat, 24 Mar 2018 23:01:37 +0100 Subject: [PATCH 04/35] Small style improvements on options dialog --- chrome/options.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chrome/options.html b/chrome/options.html index 824714d..c2b5af6 100644 --- a/chrome/options.html +++ b/chrome/options.html @@ -4,6 +4,9 @@ <title>Browserpass Chrome extension for zx2c4's pass (password manager)</title> <style> body: { padding: 10px; } + .password-store-group { margin: 10px 0; } + .password-store-path { width: 90%; } + #save { margin-top: 20px; } </style> </head> <body> @@ -43,8 +46,6 @@ <input type="checkbox" class="password-store-path-enabled" /> <input type="text" class="password-store-path" /> </div> -<br/> -<br/> <button id="save">Save</button> From 408b1a307a21cf31214004569e01e5cf261a9ea5 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Sat, 24 Mar 2018 23:22:48 +0100 Subject: [PATCH 05/35] Fix placeholder value --- chrome/options.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrome/options.html b/chrome/options.html index c2b5af6..1fe261e 100644 --- a/chrome/options.html +++ b/chrome/options.html @@ -28,7 +28,7 @@ Custom password stores directories: <br/> <div class="password-store-group"> <input type="checkbox" class="password-store-path-enabled" /> - <input type="text" class="password-store-path" placeholder="~/.password-store" /> + <input type="text" class="password-store-path" placeholder="/home/user/.password-store" /> </div> <div class="password-store-group"> <input type="checkbox" class="password-store-path-enabled" /> From f4d18df1616cb5f5929cfa651bff8b7a02eaa3e1 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 09:36:10 +1300 Subject: [PATCH 06/35] Switch options.js to browserify generation --- chrome/options.js | 45 --------------------------------------------- makefile | 5 ++++- 2 files changed, 4 insertions(+), 46 deletions(-) delete mode 100644 chrome/options.js diff --git a/chrome/options.js b/chrome/options.js deleted file mode 100644 index d97db69..0000000 --- a/chrome/options.js +++ /dev/null @@ -1,45 +0,0 @@ -function save_options() { - let autoSubmit = document.getElementById("auto-submit").checked; - localStorage.setItem("autoSubmit", autoSubmit); - - // Options related to fuzzy finding. - // use_fuzzy_search indicates if fuzzy finding or glob searching should - // be used in manual searches - let use_fuzzy = document.getElementById("use-fuzzy").checked; - localStorage.setItem("use_fuzzy_search", use_fuzzy); - - let groups = document.getElementsByClassName("password-store-group"); - let paths = []; - for (let i = 0; i < groups.length; i++) { - let group = groups[i]; - let enabled = group.querySelector(".password-store-path-enabled").checked; - let path = group.querySelector(".password-store-path").value; - if (path) { - paths.push({ path: path, enabled: enabled }); - } - } - localStorage.setItem("paths", JSON.stringify(paths)); - - window.close(); -} - -function restore_options() { - let autoSubmit = localStorage.getItem("autoSubmit") == "true"; - document.getElementById("auto-submit").checked = autoSubmit; - - // Restore the view to show the settings described above - let use_fuzzy = localStorage.getItem("use_fuzzy_search") != "false"; - document.getElementById("use-fuzzy").checked = use_fuzzy; - - let groups = document.getElementsByClassName("password-store-group"); - let paths = JSON.parse(localStorage.getItem("paths") || "[]"); - for (let i = 0; i < paths.length; i++) { - let path = paths[i]; - let group = groups[i]; - group.querySelector(".password-store-path-enabled").checked = path.enabled; - group.querySelector(".password-store-path").value = path.path; - } -} - -document.addEventListener("DOMContentLoaded", restore_options); -document.getElementById("save").addEventListener("click", save_options); diff --git a/makefile b/makefile index e9b8b72..a5c1c85 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ SHELL := /usr/bin/env bash CHROME := $(shell which google-chrome 2>/dev/null || which google-chrome-stable 2>/dev/null || which chromium 2>/dev/null || which chromium-browser 2>/dev/null || which chrome 2>/dev/null) PEM := $(shell find . -maxdepth 1 -name "*.pem") -JS_OUTPUT := chrome/script.js chrome/inject.js chrome/inject_otp.js +JS_OUTPUT := chrome/script.js chrome/options.js chrome/inject.js chrome/inject_otp.js BROWSERIFY := ./node_modules/.bin/browserify all: deps prettier js browserpass @@ -31,6 +31,9 @@ js: $(JS_OUTPUT) chrome/script.js: chrome/script.browserify.js $(BROWSERIFY) chrome/script.browserify.js -o chrome/script.js +chrome/options.js: chrome/options.browserify.js + $(BROWSERIFY) chrome/options.browserify.js -o chrome/options.js + browserpass: cmd/browserpass/ pass/ browserpass.go go build -o $@ ./cmd/browserpass From 34af039b6d498e7fa54e397a1ac38942296e7471 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 11:36:40 +1300 Subject: [PATCH 07/35] Rewrite options screen --- .gitignore | 1 + chrome/options.browserify.js | 121 +++++++++++++++++++++++++++++++++++ chrome/options.html | 97 +++++++++++++--------------- 3 files changed, 168 insertions(+), 51 deletions(-) create mode 100644 chrome/options.browserify.js diff --git a/.gitignore b/.gitignore index 0868f90..1842a42 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ firefox/* !firefox/host.json !firefox/manifest.json chrome/script.js +chrome/options.js node_modules .vagrant/ vendor/ diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js new file mode 100644 index 0000000..f777878 --- /dev/null +++ b/chrome/options.browserify.js @@ -0,0 +1,121 @@ +var dirty = []; +var settings = { + autoSubmit: { + type: "checkbox", + title: "Automatically submit forms after filling", + value: false + }, + use_fuzzy_search: { + type: "checkbox", + title: "Use fuzzy search", + value: true + }, + customStore: { + title: "Custom password store locations", + value: [{name: "one", enabled: true, name: "fish", path: "/home/fish"}], + } +}; + +// load settings & create render tree +loadSettings(); +var tree = { + view: function() { + var nodes = [m("h3", "Basic Settings")]; + for (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 (key in settings.customStore.value) { + nodes.push(createCustomStore(key, settings.customStore.value[key])); + } + nodes.push(m("a", { + onclick: function() { + settings.customStore.value.push({enabled: true, name: "", path: ""}); + saveSetting("customStore"); + } + }, "Add Store")); + return nodes; + } +}; + +// attach tree +var m = require("mithril"); +m.mount(document.body, tree); + +// load settings from local storage +function loadSettings() { + for (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("customStore"); + } + }), + m("input[type=text].name", { + title: "The name for this password store", + value: store.name, + placeholder: "personal", + onchange: function(e) { + store.name = e.target.value; + saveSetting("customStore"); + } + }), + m("input[type=text].path", { + title: "The full path to this password store", + value: store.path, + placeholder: "personal", + onchange: function(e) { + store.path = e.target.value; + saveSetting("customStore"); + } + }), + m("a.remove", { + title: "Remove this password store", + onclick: function() { + delete settings.customStore.value[key]; + saveSetting("customStore"); + } + }, "[X]") + ]); +} diff --git a/chrome/options.html b/chrome/options.html index 1fe261e..63ad137 100644 --- a/chrome/options.html +++ b/chrome/options.html @@ -1,54 +1,49 @@ <!DOCTYPE html> <html> -<head> - <title>Browserpass Chrome extension for zx2c4's pass (password manager)</title> - <style> - body: { padding: 10px; } - .password-store-group { margin: 10px 0; } - .password-store-path { width: 90%; } - #save { margin-top: 20px; } - </style> -</head> -<body> - -<label> - <input type="checkbox" id="auto-submit" /> - Auto-Submit login forms -</label> -<br/> -<br/> - -<label> - <input type="checkbox" id="use-fuzzy" /> - Use fuzzy search -</label> -<br/> -<br/> - -Custom password stores directories: <br/> -<div class="password-store-group"> - <input type="checkbox" class="password-store-path-enabled" /> - <input type="text" class="password-store-path" placeholder="/home/user/.password-store" /> -</div> -<div class="password-store-group"> - <input type="checkbox" class="password-store-path-enabled" /> - <input type="text" class="password-store-path" /> -</div> -<div class="password-store-group"> - <input type="checkbox" class="password-store-path-enabled" /> - <input type="text" class="password-store-path" /> -</div> -<div class="password-store-group"> - <input type="checkbox" class="password-store-path-enabled" /> - <input type="text" class="password-store-path" /> -</div> -<div class="password-store-group"> - <input type="checkbox" class="password-store-path-enabled" /> - <input type="text" class="password-store-path" /> -</div> - -<button id="save">Save</button> - -<script src="options.js"></script> -</body> + <head> + <title>Browserpass Chrome extension for zx2c4's pass (password manager)</title> + <style> + .option { + display: flex; + height: 16px; + line-height: 16px; + margin-bottom: 8px; + } + .option:last-child { + margin-bottom: 0; + } + .option input[type=checkbox] { + height: 12px; + margin: 2px 6px 2px 0; + padding: 0; + } + .option.custom-store input[type=text] { + border: none; + border-bottom: 1px solid #AAA; + height: 16px; + line-height: 16px; + margin: -4px 0 0 0; + overflow: hidden; + padding: 0; + width: 25%; + } + .option.custom-store input[type=text].path { + margin-left: 6px; + width: calc(100% - 25% - 42px); + } + .option.custom-store a.remove { + color: #F00; + display: block; + height: 16px; + line-height: 16px; + margin: 0; + padding: 0; + text-decoration: none; + width: 16px; + } + </style> + </head> + <body> + <script src="options.js"></script> + </body> </html> From 16b261d1c9b04c0da30bc016d1d58c437bc7d162 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 14:34:20 +1300 Subject: [PATCH 08/35] Properly declare local variables as local --- chrome/options.browserify.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js index f777878..2a0a018 100644 --- a/chrome/options.browserify.js +++ b/chrome/options.browserify.js @@ -21,14 +21,14 @@ loadSettings(); var tree = { view: function() { var nodes = [m("h3", "Basic Settings")]; - for (key in 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 (key in settings.customStore.value) { + for (var key in settings.customStore.value) { nodes.push(createCustomStore(key, settings.customStore.value[key])); } nodes.push(m("a", { @@ -47,7 +47,7 @@ m.mount(document.body, tree); // load settings from local storage function loadSettings() { - for (key in settings) { + for (var key in settings) { var value = localStorage.getItem(key); if (value !== null) { settings[key].value = JSON.parse(value); From a665883a02d622ab5371d3fd7f4e92596e1cdc54 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 14:34:45 +1300 Subject: [PATCH 09/35] Add some spacing --- chrome/options.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chrome/options.html b/chrome/options.html index 63ad137..6f9a164 100644 --- a/chrome/options.html +++ b/chrome/options.html @@ -29,14 +29,14 @@ } .option.custom-store input[type=text].path { margin-left: 6px; - width: calc(100% - 25% - 42px); + width: calc(100% - 25% - 48px); } .option.custom-store a.remove { color: #F00; display: block; height: 16px; line-height: 16px; - margin: 0; + margin: 0 0 0 6px; padding: 0; text-decoration: none; width: 16px; From 38799f53d3dbf3d286f80f3e4c6dbb5f079be9a3 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 14:37:10 +1300 Subject: [PATCH 10/35] Use getSettings() instead of directly reading from local storage --- chrome/background.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/chrome/background.js b/chrome/background.js index c548ca8..fbee013 100644 --- a/chrome/background.js +++ b/chrome/background.js @@ -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, { @@ -27,7 +21,7 @@ function fillLoginForm(login, tab) { function() { chrome.tabs.executeScript({ allFrames: true, - code: `browserpassFillForm(${loginParam}, ${autoSubmitParam});` + code: `browserpassFillForm(${loginParam}, ${getSettings().autoSubmit});` }); } ); From 6259c7c5dd529e7f542489281c95cc93917596e2 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 14:37:48 +1300 Subject: [PATCH 11/35] Define getSettings in a more general manner --- chrome/background.js | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/chrome/background.js b/chrome/background.js index fbee013..27ac56e 100644 --- a/chrome/background.js +++ b/chrome/background.js @@ -114,11 +114,33 @@ function onMessage(request, sender, sendResponse) { } function getSettings() { - const use_fuzzy_search = localStorage.getItem("use_fuzzy_search") != "false"; - const paths = JSON.parse(localStorage.getItem("paths") || "[]") - .filter(path => path.enabled) - .map(path => path.path); - return { paths: paths, use_fuzzy_search: use_fuzzy_search }; + // default settings + var settings = { + autoSubmit: false, + use_fuzzy_search: true, + customStore: [] + }; + + // 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.customStore = settings.customStore + .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 From 6d6508ad6101fde5afd7d98a30dc2118c4126836 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 14:38:50 +1300 Subject: [PATCH 12/35] Use a list of named password stores --- browserpass.go | 8 +++++--- pass/disk.go | 52 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/browserpass.go b/browserpass.go index b44f126..04752ee 100644 --- a/browserpass.go +++ b/browserpass.go @@ -33,10 +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"` - Paths []string `json:"paths"` + UseFuzzy bool `json:"use_fuzzy_search"` + CustomStore []pass.StoreDefinition `json:"customStore"` } // msg defines a message sent from a browser extension. @@ -66,7 +68,7 @@ func Run(stdin io.Reader, stdout io.Writer) error { return err } - s, err := pass.NewDefaultStore(data.Settings.Paths, data.Settings.UseFuzzy) + s, err := pass.NewDefaultStore(data.Settings.CustomStore, data.Settings.UseFuzzy) if err != nil { return err } diff --git a/pass/disk.go b/pass/disk.go index 39a9761..edb76e6 100644 --- a/pass/disk.go +++ b/pass/disk.go @@ -14,46 +14,51 @@ import ( sfuzzy "github.com/sahilm/fuzzy" ) +// StoreDefinition defines a password store object +type StoreDefinition struct { + Name string `json:"name"` + Path string `json:"path"` +} + type diskStore struct { - paths []string + Stores []StoreDefinition useFuzzy bool // Setting for FuzzySearch or GlobSearch in manual searches } -func NewDefaultStore(paths []string, useFuzzy bool) (Store, error) { - if paths == nil || len(paths) == 0 { - defaultPaths, err := defaultStorePath() +func NewDefaultStore(stores []StoreDefinition, useFuzzy bool) (Store, error) { + if stores == nil || len(stores) == 0 { + defaultPath, err := defaultStorePath() if err != nil { return nil, err } - paths = defaultPaths + stores = []StoreDefinition{{Name: "default", Path: defaultPath}} } // Follow symlinks - finalPaths := make([]string, len(paths)) - for i, path := range paths { - finalPath, err := filepath.EvalSymlinks(path) + for i, store := range stores { + finalPath, err := filepath.EvalSymlinks(store.Path) if err != nil { return nil, err } - finalPaths[i] = finalPath + stores[i].Path = finalPath } - return &diskStore{finalPaths, useFuzzy}, nil + return &diskStore{stores, useFuzzy}, nil } -func defaultStorePath() ([]string, error) { +func defaultStorePath() (string, error) { path := os.Getenv("PASSWORD_STORE_DIR") if path != "" { - return []string{path}, nil + return path, nil } usr, err := user.Current() if err != nil { - return nil, err + return "", err } path = filepath.Join(usr.HomeDir, ".password-store") - return []string{path}, nil + return path, nil } // Do a search. Will call into the correct algoritm (glob or fuzzy) @@ -94,13 +99,13 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { items := []string{} - for _, path := range s.paths { - matches, err := zglob.GlobFollowSymlinks(path + "/**/" + query + "*/**/*.gpg") + for _, store := range s.Stores { + matches, err := zglob.GlobFollowSymlinks(store.Path + "/**/" + query + "*/**/*.gpg") if err != nil { return nil, err } - matches2, err := zglob.GlobFollowSymlinks(path + "/**/" + query + "*.gpg") + matches2, err := zglob.GlobFollowSymlinks(store.Path + "/**/" + query + "*.gpg") if err != nil { return nil, err } @@ -109,11 +114,11 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { for i, match := range allMatches { // TODO this does not handle identical file names in multiple s.paths - item, err := filepath.Rel(path, match) + item, err := filepath.Rel(store.Path, match) if err != nil { return nil, err } - allMatches[i] = strings.TrimSuffix(item, ".gpg") + allMatches[i] = store.Name + ":" + strings.TrimSuffix(item, ".gpg") } items = append(items, allMatches...) @@ -136,8 +141,13 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { } func (s *diskStore) Open(item string) (io.ReadCloser, error) { - for _, path := range s.paths { - path := filepath.Join(path, item+".gpg") + parts := strings.SplitN(item, ":", 2); + + for _, store := range s.Stores { + if (store.Name != parts[0]) { + continue; + } + path := filepath.Join(store.Path, parts[1] + ".gpg") f, err := os.Open(path) if os.IsNotExist(err) { continue From 049494d3ec8758e79bd27142f1f3bb0529faaa17 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 15:05:05 +1300 Subject: [PATCH 13/35] Multi-store display mode --- chrome/script.browserify.js | 13 ++++++++++++- chrome/styles.css | 19 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index 3f9f8ea..ccf3b31 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -42,6 +42,14 @@ function view() { title: "Fill form" + (autoSubmit ? " and submit" : "") }; + var store = "default"; + var name = login; + var i; + if (i = login.indexOf(':')) { + store = login.substr(0, i); + name = login.substr(++i); + } + let faviconUrl = getFaviconUrl(domain); if (faviconUrl) { selector += ".favicon"; @@ -49,7 +57,10 @@ function view() { } return m("div.entry", [ - m(selector, options, login), + m(selector, options, [ + store != "default" ? m("div.store", store) : null, + m("div.name", name) + ]), m("button.launch.url", { onclick: launchURL.bind({ entry: login, what: "url" }), title: "Visit URL", diff --git a/chrome/styles.css b/chrome/styles.css index c11e54f..9508024 100644 --- a/chrome/styles.css +++ b/chrome/styles.css @@ -82,10 +82,11 @@ body { } .login { + display: flex; flex: 1; -webkit-appearance: none; -moz-appearance: none; - padding: 12px; + padding: 0 0 0 12px; cursor: pointer; border: 0; text-align: left; @@ -102,6 +103,22 @@ body { padding-left: 32px; } +.login .store { + background-color: #090; + border-radius: 4px; + color: #FFF; + height: calc(100% - 24px); + line-height: 100%; + margin: 10px 4px 10px 0; + padding: 2px 4px; +} + +.login .name { + height: 100%; + line-height: 100%; + padding: 12px 0; +} + .entry:last-of-type { border-bottom: 0; } From d12a9489b01c2dfdcda522d991a3bec1b9a4c471 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 15:16:09 +1300 Subject: [PATCH 14/35] Don't display the store badge if the store name isn't present --- chrome/script.browserify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index ccf3b31..0322f55 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -58,7 +58,7 @@ function view() { return m("div.entry", [ m(selector, options, [ - store != "default" ? m("div.store", store) : null, + (i > 0 && store != "default") ? m("div.store", store) : null, m("div.name", name) ]), m("button.launch.url", { From 1b6df1c58defc6516ed7f2853833009b427888cb Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 15:20:30 +1300 Subject: [PATCH 15/35] Increase the minimum width of the popup by 70px --- chrome/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrome/styles.css b/chrome/styles.css index 9508024..7e33828 100644 --- a/chrome/styles.css +++ b/chrome/styles.css @@ -2,7 +2,7 @@ body { padding: 0; margin: 0; font-family: sans-serif; - min-width: 280px; + min-width: 350px; background: white; font-size: 14px; } From ca7e08488946498b9448be863ac6d2e2cb9ba535 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 17:16:28 +1300 Subject: [PATCH 16/35] Firefox UI improvements --- chrome/options.browserify.js | 2 +- chrome/options.html | 26 +++++++++++++++++++++++--- chrome/styles.css | 1 + 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js index 2a0a018..8851356 100644 --- a/chrome/options.browserify.js +++ b/chrome/options.browserify.js @@ -31,7 +31,7 @@ var tree = { for (var key in settings.customStore.value) { nodes.push(createCustomStore(key, settings.customStore.value[key])); } - nodes.push(m("a", { + nodes.push(m("button.add-store", { onclick: function() { settings.customStore.value.push({enabled: true, name: "", path: ""}); saveSetting("customStore"); diff --git a/chrome/options.html b/chrome/options.html index 6f9a164..7b56270 100644 --- a/chrome/options.html +++ b/chrome/options.html @@ -9,9 +9,6 @@ line-height: 16px; margin-bottom: 8px; } - .option:last-child { - margin-bottom: 0; - } .option input[type=checkbox] { height: 12px; margin: 2px 6px 2px 0; @@ -41,6 +38,29 @@ text-decoration: none; width: 16px; } + .add-store { + margin-top: 8px; + } + @-moz-document url-prefix() { + html, body { + box-sizing: border-box; + overflow: hidden; + } + body { + background: #FFF; + border: 1px solid #000; + font-family: sans; + margin: 2px; + padding: 12px; + } + .option.custom-store input[type=text] { + background: #FFF; + margin: 2px; + } + .option.custom-store a.remove { + font-size: 12px; + } + } </style> </head> <body> diff --git a/chrome/styles.css b/chrome/styles.css index 7e33828..bf7b9db 100644 --- a/chrome/styles.css +++ b/chrome/styles.css @@ -5,6 +5,7 @@ body { min-width: 350px; background: white; font-size: 14px; + overflow-y: hidden; } .search > form { From 6ac632f3df61546f8d20e12f9cad1aca3159836e Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 17:53:36 +1300 Subject: [PATCH 17/35] Return some error messages from the native host app --- browserpass.go | 30 +++++++++++++++++++++--------- chrome/script.browserify.js | 7 +++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/browserpass.go b/browserpass.go index 04752ee..131062b 100644 --- a/browserpass.go +++ b/browserpass.go @@ -49,6 +49,18 @@ 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) error { protector.Protect("stdio rpath proc exec") @@ -58,19 +70,19 @@ func Run(stdin io.Reader, stdout io.Writer) error { 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) } s, err := pass.NewDefaultStore(data.Settings.CustomStore, data.Settings.UseFuzzy) if err != nil { - return err + return SendError(err, stdout) } var resp interface{} @@ -78,36 +90,36 @@ func Run(stdin io.Reader, stdout io.Writer) error { 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 { diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index 0322f55..b3295c5 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -226,6 +226,13 @@ function searchPassword(_domain, action = "search", useFillOnSubmit = true) { } searching = false; + + if (typeof(response) == "string") { + error = response; + m.redraw(); + return; + } + logins = resultLogins = response ? response : []; document.getElementById("filter-search").textContent = domain; fillOnSubmit = useFillOnSubmit && logins.length > 0; From 86a7dede538f3b7b5860b5f6f3d67f592cb1d883 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 18:04:42 +1300 Subject: [PATCH 18/35] Don't search empty or ignored domains --- chrome/script.browserify.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index b3295c5..a17d5ca 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -205,6 +205,13 @@ function init(tab) { } function searchPassword(_domain, action = "search", useFillOnSubmit = true) { + // don't run searches for empty queries or ignored URLs + _domain = _domain.trim(); + var ignore = ["newtab", "extensions"] + if (!_domain.length || ignore.indexOf(_domain) >= 0) { + return; + } + searching = true; logins = resultLogins = []; domain = _domain; From 696ff4aa1f16384b972b4b98f0b51bf86ed70bfb Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 18:18:20 +1300 Subject: [PATCH 19/35] Use settings object from search --- chrome/script.browserify.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index a17d5ca..ac6951f 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -11,6 +11,7 @@ var logins = []; var fillOnSubmit = false; var error; var domain, urlDuringSearch; +var searchSettings; m.mount(document.getElementById("mount"), { view: view, oncreate: oncreate }); @@ -34,12 +35,11 @@ function view() { m.trust(`No matching passwords found for <strong>${domain}</strong>.`) ); } else if (logins.length > 0) { - const autoSubmit = localStorage.getItem("autoSubmit") == "true"; results = logins.map(function(login) { let selector = "button.login"; let options = { onclick: getLoginData.bind(login), - title: "Fill form" + (autoSubmit ? " and submit" : "") + title: "Fill form" + (searchSettings.autoSubmit ? " and submit" : "") }; var store = "default"; @@ -223,6 +223,7 @@ function searchPassword(_domain, action = "search", useFillOnSubmit = true) { // to the settings). Then construct the message to send to browserpass and // send that via sendNativeMessage. chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) { + searchSettings = settings; chrome.runtime.sendNativeMessage( app, { action: action, domain: _domain, settings: settings }, From c85ad591b5d2cd92f53fcbe7056a95d3d8ba67c8 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 18:18:40 +1300 Subject: [PATCH 20/35] Don't show badges if there is only one custom store configured --- chrome/script.browserify.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index ac6951f..dc8e28e 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -46,7 +46,9 @@ function view() { var name = login; var i; if (i = login.indexOf(':')) { - store = login.substr(0, i); + if (searchSettings.customStore.length > 1) { + store = login.substr(0, i); + } name = login.substr(++i); } From 173e554f57dee3c619df18494cbb0681091e0ccf Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 18:26:46 +1300 Subject: [PATCH 21/35] Remove unused variable --- chrome/options.browserify.js | 1 - 1 file changed, 1 deletion(-) diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js index 8851356..3d3a093 100644 --- a/chrome/options.browserify.js +++ b/chrome/options.browserify.js @@ -1,4 +1,3 @@ -var dirty = []; var settings = { autoSubmit: { type: "checkbox", From 8c8ca8f5637efe21385c17041199d27eb1da97ec Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 18:40:22 +1300 Subject: [PATCH 22/35] Remove prefix when detecting the URL from the path --- chrome/script.browserify.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index dc8e28e..4f0e4cc 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -290,7 +290,8 @@ function launchURL() { } // get url from login path if not available in the host app response if (!response.hasOwnProperty("url") || response.url.length == 0) { - var parts = entry.split(/\//).reverse(); + var parts = (entry.indexOf(":") > 0) ? entry.substr(entry.indexOf(":") + 1) : entry; + parts = parts.split(/\//).reverse(); for (var i in parts) { var part = parts[i]; var info = Tldjs.parse(part); From abe6a7cbf6ab087e2b065b7a8bc852711b0c9e33 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 18:46:53 +1300 Subject: [PATCH 23/35] Fix hints --- chrome/options.browserify.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js index 3d3a093..b2b8d8b 100644 --- a/chrome/options.browserify.js +++ b/chrome/options.browserify.js @@ -94,7 +94,7 @@ function createCustomStore(key, store) { m("input[type=text].name", { title: "The name for this password store", value: store.name, - placeholder: "personal", + placeholder: "name", onchange: function(e) { store.name = e.target.value; saveSetting("customStore"); @@ -103,7 +103,7 @@ function createCustomStore(key, store) { m("input[type=text].path", { title: "The full path to this password store", value: store.path, - placeholder: "personal", + placeholder: "/path/to/store", onchange: function(e) { store.path = e.target.value; saveSetting("customStore"); From af8df7e6945b656197092f5b2e4bc24de5f0d6c8 Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 21:19:16 +1300 Subject: [PATCH 24/35] Remove the fish --- chrome/options.browserify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js index b2b8d8b..4ee1ac0 100644 --- a/chrome/options.browserify.js +++ b/chrome/options.browserify.js @@ -11,7 +11,7 @@ var settings = { }, customStore: { title: "Custom password store locations", - value: [{name: "one", enabled: true, name: "fish", path: "/home/fish"}], + value: [], } }; From c1d750a64bcf615bd127a8fe1af7ee64928a153e Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 21:20:50 +1300 Subject: [PATCH 25/35] Remove TODO comment --- pass/disk.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pass/disk.go b/pass/disk.go index edb76e6..7500497 100644 --- a/pass/disk.go +++ b/pass/disk.go @@ -113,7 +113,6 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { allMatches := append(matches, matches2...) for i, match := range allMatches { - // TODO this does not handle identical file names in multiple s.paths item, err := filepath.Rel(store.Path, match) if err != nil { return nil, err From 6e9e96ff3df5033d49b3d395b0630795a7de86bc Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 21:22:05 +1300 Subject: [PATCH 26/35] Remove TODO comment --- pass/disk.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pass/disk.go b/pass/disk.go index 7500497..7d07060 100644 --- a/pass/disk.go +++ b/pass/disk.go @@ -151,7 +151,6 @@ func (s *diskStore) Open(item string) (io.ReadCloser, error) { if os.IsNotExist(err) { continue } - // TODO this does not handle identical file names in multiple s.paths return f, err } return nil, errors.New("Unable to find the item on disk") From d1178ed4a89179ef7f0d01038f29580bf5ca6aba Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 21:34:05 +1300 Subject: [PATCH 27/35] Use plural name for variable --- browserpass.go | 6 +++--- chrome/background.js | 4 ++-- chrome/options.browserify.js | 20 ++++++++++---------- chrome/script.browserify.js | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/browserpass.go b/browserpass.go index 131062b..417de92 100644 --- a/browserpass.go +++ b/browserpass.go @@ -37,8 +37,8 @@ var endianness = binary.LittleEndian // 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"` - CustomStore []pass.StoreDefinition `json:"customStore"` + UseFuzzy bool `json:"use_fuzzy_search"` + CustomStores []pass.StoreDefinition `json:"customStores"` } // msg defines a message sent from a browser extension. @@ -80,7 +80,7 @@ func Run(stdin io.Reader, stdout io.Writer) error { return SendError(err, stdout) } - s, err := pass.NewDefaultStore(data.Settings.CustomStore, data.Settings.UseFuzzy) + s, err := pass.NewDefaultStore(data.Settings.CustomStores, data.Settings.UseFuzzy) if err != nil { return SendError(err, stdout) } diff --git a/chrome/background.js b/chrome/background.js index 27ac56e..6f6c722 100644 --- a/chrome/background.js +++ b/chrome/background.js @@ -118,7 +118,7 @@ function getSettings() { var settings = { autoSubmit: false, use_fuzzy_search: true, - customStore: [] + customStores: [] }; // load settings from local storage @@ -130,7 +130,7 @@ function getSettings() { } // filter custom stores by enabled & path length, and ensure they are named - settings.customStore = settings.customStore + settings.customStores = settings.customStores .filter(store => store.enabled && store.path.length > 0) .map(function(store) { if (!store.name) { diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js index 4ee1ac0..6af9af9 100644 --- a/chrome/options.browserify.js +++ b/chrome/options.browserify.js @@ -9,7 +9,7 @@ var settings = { title: "Use fuzzy search", value: true }, - customStore: { + customStores: { title: "Custom password store locations", value: [], } @@ -27,13 +27,13 @@ var tree = { } } nodes.push(m("h3", "Custom Store Locations")); - for (var key in settings.customStore.value) { - nodes.push(createCustomStore(key, settings.customStore.value[key])); + for (var key in settings.customStores.value) { + nodes.push(createCustomStore(key, settings.customStores.value[key])); } nodes.push(m("button.add-store", { onclick: function() { - settings.customStore.value.push({enabled: true, name: "", path: ""}); - saveSetting("customStore"); + settings.customStores.value.push({enabled: true, name: "", path: ""}); + saveSetting("customStores"); } }, "Add Store")); return nodes; @@ -88,7 +88,7 @@ function createCustomStore(key, store) { checked: store.enabled, onchange: function(e) { store.enabled = e.target.checked; - saveSetting("customStore"); + saveSetting("customStores"); } }), m("input[type=text].name", { @@ -97,7 +97,7 @@ function createCustomStore(key, store) { placeholder: "name", onchange: function(e) { store.name = e.target.value; - saveSetting("customStore"); + saveSetting("customStores"); } }), m("input[type=text].path", { @@ -106,14 +106,14 @@ function createCustomStore(key, store) { placeholder: "/path/to/store", onchange: function(e) { store.path = e.target.value; - saveSetting("customStore"); + saveSetting("customStores"); } }), m("a.remove", { title: "Remove this password store", onclick: function() { - delete settings.customStore.value[key]; - saveSetting("customStore"); + delete settings.customStores.value[key]; + saveSetting("customStores"); } }, "[X]") ]); diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index 4f0e4cc..7cb8549 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -46,7 +46,7 @@ function view() { var name = login; var i; if (i = login.indexOf(':')) { - if (searchSettings.customStore.length > 1) { + if (searchSettings.customStores.length > 1) { store = login.substr(0, i); } name = login.substr(++i); From f686716ef31fd302040974694cde2a91b31b5a4d Mon Sep 17 00:00:00 2001 From: Erayd <steve@erayd.net> Date: Tue, 27 Mar 2018 21:45:59 +1300 Subject: [PATCH 28/35] Add default empty entry --- chrome/options.browserify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js index 6af9af9..a8cd823 100644 --- a/chrome/options.browserify.js +++ b/chrome/options.browserify.js @@ -11,7 +11,7 @@ var settings = { }, customStores: { title: "Custom password store locations", - value: [], + value: [{enabled: true, name: "", path: ""}], } }; From 2dea7c0c5d84cbea1d8ba36ec0699b079952e1bc Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Tue, 27 Mar 2018 11:01:36 +0200 Subject: [PATCH 29/35] Resolve conflict with master --- cmd/browserpass/main.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmd/browserpass/main.go b/cmd/browserpass/main.go index 0345e5e..c34d7c2 100644 --- a/cmd/browserpass/main.go +++ b/cmd/browserpass/main.go @@ -1,6 +1,8 @@ package main import ( + "flag" + "fmt" "io" "log" "os" @@ -9,10 +11,19 @@ import ( "github.com/dannyvankooten/browserpass/protector" ) +const VERSION = "2.0.17" + func main() { protector.Protect("stdio rpath proc exec getpw") log.SetPrefix("[Browserpass] ") + showVersion := flag.Bool("v", false, "print version and exit") + flag.Parse() + if *showVersion { + fmt.Println("Browserpass host app version:", VERSION) + os.Exit(0) + } + if err := browserpass.Run(os.Stdin, os.Stdout); err != nil && err != io.EOF { log.Fatal(err) } From 31b2a0c1fd7bf1cafb51fc5c39214c0aae7eb6b7 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Tue, 27 Mar 2018 11:19:48 +0200 Subject: [PATCH 30/35] Allow using environment vars and tilde in custom stores --- pass/disk.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/pass/disk.go b/pass/disk.go index 7d07060..68bd7f2 100644 --- a/pass/disk.go +++ b/pass/disk.go @@ -16,8 +16,8 @@ import ( // StoreDefinition defines a password store object type StoreDefinition struct { - Name string `json:"name"` - Path string `json:"path"` + Name string `json:"name"` + Path string `json:"path"` } type diskStore struct { @@ -34,13 +34,18 @@ func NewDefaultStore(stores []StoreDefinition, useFuzzy bool) (Store, error) { stores = []StoreDefinition{{Name: "default", Path: defaultPath}} } - // Follow symlinks + // Expand paths, follow symlinks for i, store := range stores { - finalPath, err := filepath.EvalSymlinks(store.Path) + path := store.Path + if strings.HasPrefix(path, "~/") { + path = filepath.Join("$HOME", path[2:]) + } + path = os.ExpandEnv(path) + path, err := filepath.EvalSymlinks(path) if err != nil { return nil, err } - stores[i].Path = finalPath + stores[i].Path = path } return &diskStore{stores, useFuzzy}, nil @@ -93,7 +98,7 @@ func (s *diskStore) FuzzySearch(query string) ([]string, error) { func (s *diskStore) GlobSearch(query string) ([]string, error) { // Search: - // 1. DOMAIN/USERNAME.gpg + // 1. DOMAIN/USERNAME.gpg // 2. DOMAIN.gpg // 3. DOMAIN/SUBDIRECTORY/USERNAME.gpg @@ -140,13 +145,13 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { } func (s *diskStore) Open(item string) (io.ReadCloser, error) { - parts := strings.SplitN(item, ":", 2); + parts := strings.SplitN(item, ":", 2) for _, store := range s.Stores { - if (store.Name != parts[0]) { - continue; + if store.Name != parts[0] { + continue } - path := filepath.Join(store.Path, parts[1] + ".gpg") + path := filepath.Join(store.Path, parts[1]+".gpg") f, err := os.Open(path) if os.IsNotExist(err) { continue From a9a4617e737ea5c45005ed6b845c25a262632a03 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Tue, 27 Mar 2018 11:20:40 +0200 Subject: [PATCH 31/35] Run prettier --- chrome/background.js | 3 +-- chrome/options.browserify.js | 48 +++++++++++++++++++++++------------- chrome/script.browserify.js | 27 +++++++++++--------- chrome/styles.css | 2 +- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/chrome/background.js b/chrome/background.js index 6f6c722..7c1371e 100644 --- a/chrome/background.js +++ b/chrome/background.js @@ -137,8 +137,7 @@ function getSettings() { store.name = store.path; } return store; - }) - ; + }); return settings; } diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js index a8cd823..16801d7 100644 --- a/chrome/options.browserify.js +++ b/chrome/options.browserify.js @@ -11,7 +11,7 @@ var settings = { }, customStores: { title: "Custom password store locations", - value: [{enabled: true, name: "", path: ""}], + value: [{ enabled: true, name: "", path: "" }] } }; @@ -30,12 +30,22 @@ var tree = { 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")); + nodes.push( + m( + "button.add-store", + { + onclick: function() { + settings.customStores.value.push({ + enabled: true, + name: "", + path: "" + }); + saveSetting("customStores"); + } + }, + "Add Store" + ) + ); return nodes; } }; @@ -66,7 +76,7 @@ function saveSetting(name) { // create a checkbox option function createCheckbox(name, option) { - return m("div.option", {class: name}, [ + return m("div.option", { class: name }, [ m("input[type=checkbox]", { name: name, title: option.title, @@ -76,13 +86,13 @@ function createCheckbox(name, option) { saveSetting(name); } }), - m("label", {for: name}, option.title) + 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}, [ + return m("div.option.custom-store", { class: "store-" + store.name }, [ m("input[type=checkbox]", { title: "Whether to enable this password store", checked: store.enabled, @@ -109,12 +119,16 @@ function createCustomStore(key, store) { saveSetting("customStores"); } }), - m("a.remove", { - title: "Remove this password store", - onclick: function() { - delete settings.customStores.value[key]; - saveSetting("customStores"); - } - }, "[X]") + m( + "a.remove", + { + title: "Remove this password store", + onclick: function() { + delete settings.customStores.value[key]; + saveSetting("customStores"); + } + }, + "[X]" + ) ]); } diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js index 7cb8549..a12e9ca 100644 --- a/chrome/script.browserify.js +++ b/chrome/script.browserify.js @@ -42,15 +42,15 @@ function view() { title: "Fill form" + (searchSettings.autoSubmit ? " and submit" : "") }; - var store = "default"; - var name = login; - var i; - if (i = login.indexOf(':')) { + var store = "default"; + var name = login; + var i; + if ((i = login.indexOf(":"))) { if (searchSettings.customStores.length > 1) { store = login.substr(0, i); } - name = login.substr(++i); - } + name = login.substr(++i); + } let faviconUrl = getFaviconUrl(domain); if (faviconUrl) { @@ -60,9 +60,9 @@ function view() { return m("div.entry", [ m(selector, options, [ - (i > 0 && store != "default") ? m("div.store", store) : null, - m("div.name", name) - ]), + i > 0 && store != "default" ? m("div.store", store) : null, + m("div.name", name) + ]), m("button.launch.url", { onclick: launchURL.bind({ entry: login, what: "url" }), title: "Visit URL", @@ -209,7 +209,7 @@ function init(tab) { function searchPassword(_domain, action = "search", useFillOnSubmit = true) { // don't run searches for empty queries or ignored URLs _domain = _domain.trim(); - var ignore = ["newtab", "extensions"] + var ignore = ["newtab", "extensions"]; if (!_domain.length || ignore.indexOf(_domain) >= 0) { return; } @@ -237,7 +237,7 @@ function searchPassword(_domain, action = "search", useFillOnSubmit = true) { searching = false; - if (typeof(response) == "string") { + if (typeof response == "string") { error = response; m.redraw(); return; @@ -290,7 +290,10 @@ function launchURL() { } // get url from login path if not available in the host app response if (!response.hasOwnProperty("url") || response.url.length == 0) { - var parts = (entry.indexOf(":") > 0) ? entry.substr(entry.indexOf(":") + 1) : entry; + var parts = + entry.indexOf(":") > 0 + ? entry.substr(entry.indexOf(":") + 1) + : entry; parts = parts.split(/\//).reverse(); for (var i in parts) { var part = parts[i]; diff --git a/chrome/styles.css b/chrome/styles.css index bf7b9db..08c0e91 100644 --- a/chrome/styles.css +++ b/chrome/styles.css @@ -107,7 +107,7 @@ body { .login .store { background-color: #090; border-radius: 4px; - color: #FFF; + color: #fff; height: calc(100% - 24px); line-height: 100%; margin: 10px 4px 10px 0; From c361f7ba8a733f7dc2fd7b797598eaad457bf664 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Tue, 27 Mar 2018 12:14:06 +0200 Subject: [PATCH 32/35] Make fuzzy search ignore store name, fix tests --- pass/disk.go | 22 ++++++++------ pass/disk_test.go | 73 +++++++++++++++++++++-------------------------- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/pass/disk.go b/pass/disk.go index 68bd7f2..e87f63a 100644 --- a/pass/disk.go +++ b/pass/disk.go @@ -21,7 +21,7 @@ type StoreDefinition struct { } type diskStore struct { - Stores []StoreDefinition + stores []StoreDefinition useFuzzy bool // Setting for FuzzySearch or GlobSearch in manual searches } @@ -79,21 +79,27 @@ func (s *diskStore) Search(query string) ([]string, error) { // for the empty string, then apply appropriate logic to convert results to // a slice of strings, finally returning all of the unique entries. func (s *diskStore) FuzzySearch(query string) ([]string, error) { - var items []string - fileList, err := s.GlobSearch("") + entries, err := s.GlobSearch("") if err != nil { return nil, err } + // GlobSearch now results `storename:filename`, for fuzzy search we need to provide only file names + var fileNames []string + for _, entry := range entries { + fileNames = append(fileNames, strings.SplitN(entry, ":", 2)[1]) + } + // The resulting match struct does not copy the strings, but rather // provides the index to the original array. Copy those strings // into the result slice - matches := sfuzzy.Find(query, fileList) + var results []string + matches := sfuzzy.Find(query, fileNames) for _, match := range matches { - items = append(items, fileList[match.Index]) + results = append(results, entries[match.Index]) } - return items, nil + return results, nil } func (s *diskStore) GlobSearch(query string) ([]string, error) { @@ -104,7 +110,7 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { items := []string{} - for _, store := range s.Stores { + for _, store := range s.stores { matches, err := zglob.GlobFollowSymlinks(store.Path + "/**/" + query + "*/**/*.gpg") if err != nil { return nil, err @@ -147,7 +153,7 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) { func (s *diskStore) Open(item string) (io.ReadCloser, error) { parts := strings.SplitN(item, ":", 2) - for _, store := range s.Stores { + for _, store := range s.stores { if store.Name != parts[0] { continue } diff --git a/pass/disk_test.go b/pass/disk_test.go index 7ea51d8..b8f4b82 100644 --- a/pass/disk_test.go +++ b/pass/disk_test.go @@ -8,8 +8,7 @@ import ( ) func TestDefaultStorePath(t *testing.T) { - var home, expectedCustom string - var expected, actual []string + var home, expectedCustom, expected, actual string usr, err := user.Current() @@ -21,44 +20,36 @@ func TestDefaultStorePath(t *testing.T) { // default directory os.Setenv("PASSWORD_STORE_DIR", "") - expected = []string{filepath.Join(home, ".password-store")} + expected = filepath.Join(home, ".password-store") actual, _ = defaultStorePath() - if len(expected) != len(actual) { - t.Errorf("1: '%d' does not match '%d'", len(expected), len(actual)) - } - - if expected[0] != actual[0] { - t.Errorf("2: '%s' does not match '%s'", expected[0], actual[0]) + if expected != actual { + t.Errorf("1: '%s' does not match '%s'", expected, actual) } // custom directory from $PASSWORD_STORE_DIR - expectedCustom, err = filepath.Abs("browserpass-test") + expected, err = filepath.Abs("browserpass-test") if err != nil { t.Error(err) } - expected = []string{expectedCustom} os.Mkdir(expectedCustom, os.ModePerm) - os.Setenv("PASSWORD_STORE_DIR", expectedCustom) + os.Setenv("PASSWORD_STORE_DIR", expected) actual, err = defaultStorePath() if err != nil { t.Error(err) } - if len(expected) != len(actual) { - t.Errorf("3: '%d' does not match '%d'", len(expected), len(actual)) - } - if expected[0] != actual[0] { - t.Errorf("4: '%s' does not match '%s'", expected[0], actual[0]) + if expected != actual { + t.Errorf("2: '%s' does not match '%s'", expected, actual) } // clean-up os.Setenv("PASSWORD_STORE_DIR", "") - os.Remove(expected[0]) + os.Remove(expected) } func TestDiskStore_Search_nomatch(t *testing.T) { - store := diskStore{[]string{"test_store"}, false} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} domain := "this-most-definitely-does-not-exist" logins, err := store.Search(domain) @@ -71,8 +62,8 @@ func TestDiskStore_Search_nomatch(t *testing.T) { } func TestDiskStoreSearch(t *testing.T) { - store := diskStore{[]string{"test_store"}, false} - targetDomain := "abc.com" + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} + expectedResult := "default:abc.com" testDomains := []string{"abc.com", "test.abc.com", "testing.test.abc.com"} for _, domain := range testDomains { searchResults, err := store.Search(domain) @@ -82,19 +73,19 @@ func TestDiskStoreSearch(t *testing.T) { // check if result contains abc.com found := false for _, searchResult := range searchResults { - if searchResult == targetDomain { + if searchResult == expectedResult { found = true break } } if found != true { - t.Fatalf("Couldn't find %v in %v", targetDomain, searchResults) + t.Fatalf("Couldn't find %v in %v", expectedResult, searchResults) } } } func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testing.T) { - store := diskStore{[]string{"test_store"}, false} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} searchResult, err := store.Search("xyz") if err != nil { t.Fatal(err) @@ -102,14 +93,14 @@ func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testi if len(searchResult) != 1 { t.Fatalf("Found %v results instead of 1", len(searchResult)) } - expectedResult := "xyz.com/xyz_user" + expectedResult := "default:xyz.com/xyz_user" if searchResult[0] != expectedResult { t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult[0]) } } func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) { - store := diskStore{[]string{"test_store"}, false} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} searchResult, err := store.Search("def.com") if err != nil { t.Fatal(err) @@ -117,14 +108,14 @@ func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) { if len(searchResult) != 1 { t.Fatalf("Found %v results instead of 1", len(searchResult)) } - expectedResult := "def.com" + expectedResult := "default:def.com" if searchResult[0] != expectedResult { t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult[0]) } } func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) { - store := diskStore{[]string{"test_store"}, false} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} searchResult, err := store.Search("amazon.co.uk") if err != nil { t.Fatal(err) @@ -132,18 +123,18 @@ func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) { if len(searchResult) != 2 { t.Fatalf("Found %v results instead of 2", len(searchResult)) } - expectedResult := []string{"amazon.co.uk/user1", "amazon.co.uk/user2"} + expectedResult := []string{"default:amazon.co.uk/user1", "default:amazon.co.uk/user2"} if searchResult[0] != expectedResult[0] || searchResult[1] != expectedResult[1] { t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult) } } func TestDiskStoreSearchSubDirectories(t *testing.T) { - store := diskStore{[]string{"test_store"}, false} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} searchTermsMatches := map[string][]string{ - "abc.org": []string{"abc.org/user3", "abc.org/wiki/user4", "abc.org/wiki/work/user5"}, - "wiki": []string{"abc.org/wiki/user4", "abc.org/wiki/work/user5"}, - "work": []string{"abc.org/wiki/work/user5"}, + "abc.org": []string{"default:abc.org/user3", "default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"}, + "wiki": []string{"default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"}, + "work": []string{"default:abc.org/wiki/work/user5"}, } for term, expectedResult := range searchTermsMatches { @@ -163,7 +154,7 @@ func TestDiskStoreSearchSubDirectories(t *testing.T) { } func TestDiskStorePartSearch(t *testing.T) { - store := diskStore{[]string{"test_store"}, false} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} searchResult, err := store.Search("ab") if err != nil { t.Fatal(err) @@ -171,7 +162,7 @@ func TestDiskStorePartSearch(t *testing.T) { if len(searchResult) != 4 { t.Fatalf("Found %v results instead of 4", len(searchResult)) } - expectedResult := []string{"abc.com", "abc.org/user3", "abc.org/wiki/user4", "abc.org/wiki/work/user5"} + expectedResult := []string{"default:abc.com", "default:abc.org/user3", "default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"} for i := 0; i < len(expectedResult); i++ { if searchResult[i] != expectedResult[i] { t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult) @@ -180,7 +171,7 @@ func TestDiskStorePartSearch(t *testing.T) { } func TestFuzzySearch(t *testing.T) { - store := diskStore{[]string{"test_store"}, true} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true} searchResult, err := store.Search("amaz2") if err != nil { @@ -191,8 +182,8 @@ func TestFuzzySearch(t *testing.T) { } expectedResult := map[string]bool{ - "amazon.co.uk/user2": true, - "amazon.com/user2": true, + "default:amazon.co.uk/user2": true, + "default:amazon.com/user2": true, } for _, res := range searchResult { @@ -203,7 +194,7 @@ func TestFuzzySearch(t *testing.T) { } func TestFuzzySearchNoResult(t *testing.T) { - store := diskStore{[]string{"test_store"}, true} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true} searchResult, err := store.Search("vvv") if err != nil { @@ -215,7 +206,7 @@ func TestFuzzySearchNoResult(t *testing.T) { } func TestFuzzySearchTopLevelEntries(t *testing.T) { - store := diskStore{[]string{"test_store"}, true} + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true} searchResult, err := store.Search("def") if err != nil { @@ -226,7 +217,7 @@ func TestFuzzySearchTopLevelEntries(t *testing.T) { } expectedResult := map[string]bool{ - "def.com": true, + "default:def.com": true, } for _, res := range searchResult { From af637e656eeeece26db09d9fedb83b19edcd1681 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Tue, 27 Mar 2018 15:18:50 +0200 Subject: [PATCH 33/35] Add unit tests for multiple stores --- makefile | 2 +- pass/disk_test.go | 30 ++++++++++++++++++++++++++++++ pass/test_store_2/abc.com.gpg | 0 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 pass/test_store_2/abc.com.gpg diff --git a/makefile b/makefile index 3581b76..c8c7c8c 100644 --- a/makefile +++ b/makefile @@ -20,7 +20,7 @@ endif .PHONY: prettier prettier: - "$(PRETTIER)" --write $(PRETTIER_SOURCES) + $(PRETTIER) --write $(PRETTIER_SOURCES) .PHONY: js js: $(JS_OUTPUT) diff --git a/pass/disk_test.go b/pass/disk_test.go index b8f4b82..dcaadbd 100644 --- a/pass/disk_test.go +++ b/pass/disk_test.go @@ -226,3 +226,33 @@ func TestFuzzySearchTopLevelEntries(t *testing.T) { } } } + +func TestGlobSearchMultipleStores(t *testing.T) { + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}, StoreDefinition{Name: "custom", Path: "test_store_2"}}, useFuzzy: false} + searchResults, err := store.Search("abc.com") + if err != nil { + t.Fatal(err) + } + if len(searchResults) != 2 { + t.Fatalf("Found %v results instead of 2", len(searchResults)) + } + expectedResults := []string{"custom:abc.com", "default:abc.com"} + if searchResults[0] != expectedResults[0] || searchResults[1] != expectedResults[1] { + t.Fatalf("Couldn't find %v, found %v instead", expectedResults, searchResults) + } +} + +func TestFuzzySearchMultipleStores(t *testing.T) { + store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}, StoreDefinition{Name: "custom", Path: "test_store_2"}}, useFuzzy: true} + searchResults, err := store.Search("abc.com") + if err != nil { + t.Fatal(err) + } + if len(searchResults) != 2 { + t.Fatalf("Found %v results instead of 2", len(searchResults)) + } + expectedResults := []string{"default:abc.com", "custom:abc.com"} + if searchResults[0] != expectedResults[0] || searchResults[1] != expectedResults[1] { + t.Fatalf("Couldn't find %v, found %v instead", expectedResults, searchResults) + } +} diff --git a/pass/test_store_2/abc.com.gpg b/pass/test_store_2/abc.com.gpg new file mode 100644 index 0000000..e69de29 From 53e1cf02671788be7a2b806bedb32f565a0cd755 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Tue, 27 Mar 2018 15:38:44 +0200 Subject: [PATCH 34/35] Add tests goal to the makefile --- makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/makefile b/makefile index c8c7c8c..322a5ff 100644 --- a/makefile +++ b/makefile @@ -6,7 +6,7 @@ BROWSERIFY := node_modules/.bin/browserify PRETTIER := node_modules/.bin/prettier PRETTIER_SOURCES := $(shell find chrome -maxdepth 1 -name "*.js" -o -name "*.css") -all: deps prettier js browserpass +all: deps prettier js browserpass tests .PHONY: crx crx: @@ -53,6 +53,11 @@ browserpass-openbsd64: cmd/browserpass/ pass/ browserpass.go browserpass-freebsd64: cmd/browserpass/ pass/ browserpass.go env GOOS=freebsd GOARCH=amd64 go build -o $@ ./cmd/browserpass +.PHONY: tests +tests: + go test + go test ./pass + clean: rm -f browserpass rm -f browserpass-* From f5b3cf9cacfbec042ecf6c0de9a695faa20c9b00 Mon Sep 17 00:00:00 2001 From: Maxim Baz <git@maximbaz.com> Date: Tue, 27 Mar 2018 15:56:25 +0200 Subject: [PATCH 35/35] I don't follow the math, but this looks better in Firefox --- chrome/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrome/styles.css b/chrome/styles.css index 08c0e91..95e58b2 100644 --- a/chrome/styles.css +++ b/chrome/styles.css @@ -108,7 +108,7 @@ body { background-color: #090; border-radius: 4px; color: #fff; - height: calc(100% - 24px); + height: calc(100% - 22px); line-height: 100%; margin: 10px 4px 10px 0; padding: 2px 4px;