├── .editorconfig ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── ISSUE_TEMPLATE.md ├── LICENSE ├── Makefile ├── README.md ├── Vagrantfile ├── browserpass.go ├── browserpass_test.go ├── chrome ├── background.browserify.js ├── content.html ├── host.json ├── icon-copy.svg ├── icon-globe.svg ├── icon-key.svg ├── icon-lock.png ├── icon-lock.svg ├── icon-otp.svg ├── icon-search.svg ├── icon-user.svg ├── inject.js ├── inject_otp.js ├── manifest.json ├── options.browserify.js ├── options.css ├── options.html ├── otp.css ├── otp.html ├── otp.js ├── policy.json ├── script.browserify.js └── styles.css ├── cmd └── browserpass │ └── main.go ├── firefox ├── host.json └── manifest.json ├── install.ps1 ├── install.sh ├── package.json ├── pass ├── disk.go ├── disk_test.go ├── pass.go ├── test_store │ ├── abc.com.gpg │ ├── abc.org │ │ ├── user3.gpg │ │ └── wiki │ │ │ ├── user4.gpg │ │ │ └── work │ │ │ └── user5.gpg │ ├── amazon.co.uk │ ├── amazon.com │ │ ├── user1.gpg │ │ └── user2.gpg │ ├── def.com.gpg │ └── xyz.com │ │ └── xyz_user.gpg └── test_store_2 │ └── abc.com.gpg ├── protector ├── protector_generic.go └── protector_openbsd.go ├── uninstall.ps1 └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /browserpass 2 | /browserpass-* 3 | /private-release 4 | *.crx 5 | *.log 6 | *.pem 7 | chrome-* 8 | firefox-* 9 | release 10 | firefox/* 11 | !firefox/host.json 12 | !firefox/manifest.json 13 | chrome/background.js 14 | chrome/script.js 15 | chrome/options.js 16 | node_modules 17 | .vagrant/ 18 | vendor/ 19 | .*.swp 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | You will need Docker or Node, [Yarn](https://yarnpkg.com/), Golang and [dep](https://github.com/golang/dep) installed. 4 | 5 | ## To build 6 | - Run `make` to fetch all dependencies and compile both front-end and back-end code 7 | 8 | OR 9 | 10 | - Run `make deps` to download all dependencies (you don't need to run this very often) 11 | - Run `make js` to compile only front-end code 12 | - Run `make prettier` to additionally auto-format the code (this helps keeping code style consistent) 13 | - Run `make browserpass` to compile only back-end code 14 | 15 | The commands above will generate unpacked extensions for both Firefox and Chrome and compile the Go binaries for all supported platforms. 16 | 17 | ## To load an unpacked extension 18 | - Run `./install.sh` or `./install.ps1` to install the compiled Go binary 19 | - For Chrome: 20 | - Go to `chrome://extensions` 21 | - Enable `Developer mode` 22 | - Click `Load unpacked extension` 23 | - Select `browserpass/chrome` directory 24 | - For Firefox: 25 | - Go to `about:debugging#addons` 26 | - Click `Load temporary add-on` 27 | - Select `browserpass/firefox` directory 28 | 29 | ## Build using Docker 30 | 31 | The [Dockerfile](Dockerfile) will set up a docker image suitable for running basic make targets such as building frontend and backend. 32 | 33 | The `crx` target is not supported by now (therefore `release` target will not work). 34 | 35 | To build the docker image run the following command in project root: 36 | ```shell 37 | docker build -t browserpass-dev . 38 | ``` 39 | 40 | To build browserpass (frontend and backend) via docker, run the following from project root (this is the preferred approach): 41 | ```shell 42 | docker run --rm -v "$(pwd)":/browserpass browserpass-dev 43 | ``` 44 | 45 | If you only want a specific action, such as to download dependencies or to build front-end or backend code, use one of the following commands: 46 | ```shell 47 | docker run --rm -v "$(pwd)":/browserpass browserpass-dev deps 48 | docker run --rm -v "$(pwd)":/browserpass browserpass-dev js 49 | docker run --rm -v "$(pwd)":/browserpass browserpass-dev browserpass 50 | ``` 51 | 52 | ## Setting up a Vagrant build environment and building the Linux binary 53 | 54 | Vagrant will set up a virtual machine with all dependencies installed for you. Your local working directory is shared into the VM. 55 | These instructions will walk you through the process of setting up a build environment for browserpass using [Vagrant](https://www.vagrantup.com/) on Debian/Ubuntu. These instructions were valid for an Ubuntu 16.04 host. This only addresses building the Linux 64-bit binary - you'll need to faff around a bit to do other things, but this should provide you with a good starting point. 56 | 57 | Install vagrant: 58 | ```shell 59 | $ sudo apt-get install vagrant 60 | ``` 61 | 62 | Start the VM: 63 | ```shell 64 | $ vagrant up 65 | ``` 66 | 67 | Jump into the VM and build the project: 68 | ```shell 69 | $ vagrant ssh 70 | vagrant@minimal-xenial:~$ cd go/src/github.com/dannyvankooten/browserpass 71 | vagrant@minimal-xenial:~/go/src/github.com/dannyvankooten/browserpass$ make js 72 | vagrant@minimal-xenial:~/go/src/github.com/dannyvankooten/browserpass$ make browserpass-linux64 73 | ``` 74 | 75 | Exit the build environment, clean up the vagrant image. 76 | ```shell 77 | vagrant@minimal-xenial:~/go/src/github.com/dannyvankooten/browserpass$ exit 78 | $ vagrant destroy 79 | [ Vagrant tells you about stopping and removing the VM ] 80 | ``` 81 | 82 | ## To contribute 83 | 84 | 1. Fork [the repo](https://github.com/dannyvankooten/browserpass) 85 | 2. Create your feature branch 86 | * `git checkout -b my-new-feature` 87 | 3. Commit your changes 88 | * `git commit -am 'Add some feature'` 89 | 4. Push to the branch 90 | * `git push origin my-new-feature` 91 | 5. Create new pull Request 92 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.8-alpine3.6 2 | 3 | ENV CGO_ENABLED=0 \ 4 | APP_PATH="$GOPATH/src/github.com/dannyvankooten/browserpass" 5 | # New ENV statement as this depends on APP_PATH 6 | ENV PATH="$PATH:$APP_PATH/node_modules/.bin/" 7 | 8 | RUN apk add --no-cache bash git make tar yarn zip && \ 9 | go get -u github.com/golang/dep/cmd/dep && \ 10 | mkdir -p $APP_PATH && \ 11 | ln -s $APP_PATH / 12 | 13 | WORKDIR $APP_PATH 14 | 15 | ENTRYPOINT ["make"] 16 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/gokyle/twofactor" 6 | packages = ["."] 7 | revision = "bbc82ff8de72400ce39a13077627531d9841ad62" 8 | version = "v1.0.1" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/mattn/go-zglob" 13 | packages = [ 14 | ".", 15 | "fastwalk" 16 | ] 17 | revision = "4959821b481786922ac53e7ef25c61ae19fb7c36" 18 | 19 | [[projects]] 20 | name = "github.com/sahilm/fuzzy" 21 | packages = ["."] 22 | revision = "0e1c4a4e1f632f936a5c0fa6176076eea01c054b" 23 | version = "v0.0.3" 24 | 25 | [[projects]] 26 | branch = "master" 27 | name = "golang.org/x/sys" 28 | packages = ["unix"] 29 | revision = "3ccc7e5779793fd54564baf60c51bf017955e0ba" 30 | 31 | [[projects]] 32 | branch = "master" 33 | name = "rsc.io/qr" 34 | packages = [ 35 | ".", 36 | "coding", 37 | "gf256" 38 | ] 39 | revision = "48b2ede4844e13f1a2b7ce4d2529c9af7e359fc5" 40 | 41 | [solve-meta] 42 | analyzer-name = "dep" 43 | analyzer-version = 1 44 | inputs-digest = "d14ee1e21acbc5485af24c8de94d50a9e3f9154712df345f9320cff01f2d4a37" 45 | solver-name = "gps-cdcl" 46 | solver-version = 1 47 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | branch = "master" 26 | name = "github.com/gokyle/twofactor" 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/mattn/go-zglob" 31 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### General information 2 | 3 | 4 | * Operating system + version: 5 | * Browser + version: 6 | * Information about the host app: 7 | * How did you install it? 8 | * Installed via a package manager, downloaded a pre-built binary, compiled yourself? 9 | * If installed an official release, put a version: 10 | * In the recent versions it can be obtained with `$ browserpass -v`. 11 | * If in doubt what version you have, download and re-install the latest version! 12 | * If built from sources, put a commit id (`$ git describe --always`): 13 | * Information about the browser extension: 14 | * How did you install it? 15 | * Installed via webstore, downloaded a pre-built extension, compiled yourself? 16 | * Browserpass extension version as reported by your browser: 17 | 18 | --- 19 | 20 | ### Exact steps to reproduce the problem 21 | 1. 22 | 2. 23 | 3. 24 | 25 | ### What should happen? 26 | 27 | ### What happened instead? 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Danny van Kooten 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /usr/bin/env bash 2 | 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) 3 | PEM := $(shell find . -maxdepth 1 -name "*.pem") 4 | JS_OUTPUT := chrome/background.js chrome/script.js chrome/options.js chrome/inject.js chrome/inject_otp.js 5 | BROWSERIFY := node_modules/.bin/browserify 6 | PRETTIER := node_modules/.bin/prettier 7 | PRETTIER_SOURCES := $(shell find chrome -maxdepth 1 -name "*.js" -o -name "*.css") 8 | 9 | all: deps prettier js browserpass test 10 | 11 | .PHONY: crx 12 | crx: 13 | ifneq ($(PEM),) 14 | "$(CHROME)" --disable-gpu --pack-extension=./chrome --pack-extension-key=$(PEM) 15 | else 16 | "$(CHROME)" --disable-gpu --pack-extension=./chrome 17 | rm ./chrome.pem 18 | endif 19 | mv chrome.crx chrome-browserpass.crx 20 | 21 | .PHONY: prettier 22 | prettier: 23 | $(PRETTIER) --write $(PRETTIER_SOURCES) 24 | 25 | .PHONY: js 26 | js: $(JS_OUTPUT) 27 | cp chrome/host.json chrome-host.json 28 | cp firefox/host.json firefox-host.json 29 | cp chrome/policy.json chrome-policy.json 30 | cp chrome/{*.html,*.css,*.js,*.png,*.svg} firefox/ 31 | 32 | chrome/background.js: chrome/background.browserify.js 33 | $(BROWSERIFY) chrome/background.browserify.js -o chrome/background.js 34 | 35 | chrome/script.js: chrome/script.browserify.js 36 | $(BROWSERIFY) chrome/script.browserify.js -o chrome/script.js 37 | 38 | chrome/options.js: chrome/options.browserify.js 39 | $(BROWSERIFY) chrome/options.browserify.js -o chrome/options.js 40 | 41 | browserpass: cmd/browserpass/ pass/ browserpass.go 42 | go build -o $@ ./cmd/browserpass 43 | 44 | browserpass-linux64: cmd/browserpass/ pass/ browserpass.go 45 | env GOOS=linux GOARCH=amd64 go build -o $@ ./cmd/browserpass 46 | 47 | browserpass-windows64: cmd/browserpass/ pass/ browserpass.go 48 | env GOOS=windows GOARCH=amd64 go build -o $@.exe ./cmd/browserpass 49 | 50 | browserpass-darwinx64: cmd/browserpass/ pass/ browserpass.go 51 | env GOOS=darwin GOARCH=amd64 go build -o $@ ./cmd/browserpass 52 | 53 | browserpass-openbsd64: cmd/browserpass/ pass/ browserpass.go 54 | env GOOS=openbsd GOARCH=amd64 go build -o $@ ./cmd/browserpass 55 | 56 | browserpass-freebsd64: cmd/browserpass/ pass/ browserpass.go 57 | env GOOS=freebsd GOARCH=amd64 go build -o $@ ./cmd/browserpass 58 | 59 | .PHONY: test 60 | test: 61 | go test 62 | go test ./pass 63 | 64 | clean: 65 | rm -f browserpass 66 | rm -f browserpass-* 67 | rm -rf release 68 | rm -rf private-release 69 | git clean -fdx chrome/ 70 | git clean -fdx firefox/ 71 | rm -f *.crx 72 | rm -f *-host.json 73 | rm -f chrome-policy.json 74 | 75 | sign: release 76 | for file in release/*; do \ 77 | gpg --detach-sign "$$file"; \ 78 | done 79 | 80 | deps: 81 | yarn 82 | dep ensure -vendor-only 83 | 84 | tarball: clean deps js 85 | rm -rf /tmp/browserpass /tmp/browserpass-src.tar.gz 86 | cp -r ../browserpass /tmp/browserpass 87 | rm -rf /tmp/browserpass/.git 88 | find /tmp/browserpass -name "*.pem" -type f -delete 89 | (cd /tmp && tar -czf /tmp/browserpass-src.tar.gz browserpass) 90 | mkdir -p release 91 | cp /tmp/browserpass-src.tar.gz release/ 92 | 93 | .PHONY: release js crx 94 | release: clean deps js tarball crx browserpass-linux64 browserpass-darwinx64 browserpass-openbsd64 browserpass-freebsd64 browserpass-windows64 95 | mkdir -p private-release 96 | cp -r chrome private-release 97 | sed -i '/"key"/d' private-release/chrome/manifest.json 98 | zip -jFS private-release/chrome private-release/chrome/* key.pem 99 | rm -rf private-release/chrome 100 | mkdir -p release 101 | cp chrome-browserpass.crx release/ 102 | zip -jFS release/chrome chrome/* chrome-browserpass.crx 103 | zip -jFS release/firefox firefox/* 104 | zip -FS release/browserpass-linux64 browserpass-linux64 *-host.json chrome-policy.json chrome-browserpass.crx install.sh README.md LICENSE 105 | zip -FS release/browserpass-darwinx64 browserpass-darwinx64 *-host.json chrome-policy.json chrome-browserpass.crx install.sh README.md LICENSE 106 | zip -FS release/browserpass-openbsd64 browserpass-openbsd64 *-host.json chrome-policy.json chrome-browserpass.crx install.sh README.md LICENSE 107 | zip -FS release/browserpass-freebsd64 browserpass-freebsd64 *-host.json chrome-policy.json chrome-browserpass.crx install.sh README.md LICENSE 108 | zip -FS release/browserpass-windows64 browserpass-windows64.exe *-host.json chrome-policy.json chrome-browserpass.crx *.ps1 README.md LICENSE 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browserpass 2 | 3 | **IMPORTANT: this repository is archived and not maintained anymore.** 4 | 5 | **Browserpass was rewritten from scratch and split in two repositories:** 6 | 7 | - **Browser extension: [browserpass-extension](https://github.com/browserpass/browserpass-extension)** 8 | - **Native host app: [browserpass-native](https://github.com/browserpass/browserpass-native)** 9 | 10 | **Follow to the new repositories for installation instructions. We highly recommend to read README in both repositories to get acquainted with the new changes.** 11 | 12 | ### FAQ 13 | 14 | **1. Is the new version backwards compatible?** 15 | 16 | No, and therefore you need to update both browser extension and native host at the same time. If you installed browser extension from Web stores, it will auto-update, but you must install browserpass native host v3 yourself. 17 | 18 | Read [browserpass-native installation section](https://github.com/browserpass/browserpass-native#installation) to see if your OS provides an updated package for Browserpass v3, if not then follow manual installation steps as described in that section. 19 | 20 | **2. Can I upgrade now, and not wait for an auto-update to come?** 21 | 22 | If you use a Chromium-based browser, go to [browserpass-extension releases](https://github.com/browserpass/browserpass-extension/releases) and download the latest `browserpass-webstore.crx`. Then open `chrome://extensions`, enable "Developer mode" and drag'n'drop the downloaded `crx` file. Finally proceed to [browserpass-native installation section](https://github.com/browserpass/browserpass-native#installation) for how to install a new version of the native host. 23 | 24 | If you use Firefox, go to [browserpass-extension releases](https://github.com/browserpass/browserpass-extension/releases) and download the latest `firefox.zip` file, unpack it in a folder, then in Firefox go to `about:debugging#addons` and click on "Load Temporary Add-on" to install the extension. Finally proceed to [browserpass-native installation section](https://github.com/browserpass/browserpass-native#installation) for how to install a new version of the native host. 25 | 26 | If you unpack the contents of `firefox.zip` in `/usr/share/mozilla/extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}/browserpass@maximbaz.com/` folder, according to my experiments Firefox will treat it as persistent extension, it will ignore whatever is currently on Web Store and you will not need to load this extension after every Firefox restart. 27 | 28 | **3. Can I keep the old version, if I don't have time to upgrade native host app or if my OS hasn't updated the "browserpass" package yet?** 29 | 30 | Go to the [latest v2 release](https://github.com/browserpass/browserpass/releases), download `chrome.zip` or `firefox.zip` depending on what browser you use. Unpack the archive in a new directory, and then load this extension in the browser: 31 | 32 | In Chromium: 33 | 34 | - Go to `chrome://extensions` 35 | - Enable "Developer mode" 36 | - Click "Load unpacked" and select the folder with the unpacked contents of `chrome.zip` 37 | 38 | In Firefox: 39 | 40 | - Go to `about:debugging#addons` 41 | - Click "Load Temporary Add-on" and select the folder with the unpacked contents of `firefox.zip` 42 | 43 | If you unpack the contents of `firefox.zip` in `/usr/share/mozilla/extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}/browserpass@maximbaz.com/` folder, according to my experiments Firefox will treat it as persistent extension, it will ignore whatever is currently on Web Store and you will not need to load this extension after every Firefox restart. 44 | 45 | **4. What happened to OTP?** 46 | 47 | OTP was not implemented in Browserpass v3, but it might be implemented as a separate extension. For more details, see [Support OTP in Browserpass v3](https://github.com/browserpass/browserpass-extension/issues/76). 48 | 49 | --- 50 | 51 | Browserpass is a Chrome & Firefox extension for [zx2c4's pass](https://www.passwordstore.org/), a UNIX based password manager. It retrieves your decrypted passwords for the current domain and allows you to auto-fill login forms, as well as copy it to clipboard. If you have multiple logins for the current site, the extension shows you a list of usernames to choose from. 52 | 53 | ![Browserpass in the Chrome menu](https://user-images.githubusercontent.com/1177900/38048732-79007146-32c6-11e8-98d5-557ba3f8b262.gif) 54 | 55 | It uses a [native binary written in Golang](https://github.com/dannyvankooten/browserpass/blob/master/browserpass.go) to do the interfacing with your password store. Secure communication between the binary and the browser extension is handled through [native messaging](https://developer.chrome.com/extensions/nativeMessaging). 56 | 57 | ## Table of Contents 58 | 59 | - [Requirements](#requirements) 60 | - [Installation](#installation) 61 | - [Updates](#updates) 62 | - [Usage](#usage) 63 | - [Options](#options) 64 | - [Security](#security) 65 | - [FAQ](#faq) 66 | - [Contributing](#contributing) 67 | - [License](#license) 68 | 69 | ## Requirements 70 | 71 | - A recent version of Chrome, Chromium or Firefox 50+. 72 | - Pass (on UNIX) 73 | - Your password filename must match your username **or** your file must have a line starting with `login:`, `user:` or `username:`, followed by your username. 74 | 75 | _Examples_ 76 | 77 | ```bash 78 | $ pass website.com/johndoe 79 | the-password 80 | 81 | $ pass website.com 82 | the-password 83 | login: johndoe 84 | ``` 85 | 86 | ## Installation 87 | 88 | In order to install browserpass correctly, you have to install two of its components: 89 | 90 | - host application 91 | - browser extension(s). 92 | 93 | ### Installing the host application 94 | 95 | The following OS have a browserpass package that can be installed via package manager: 96 | 97 | - [Arch Linux](https://aur.archlinux.org/packages/browserpass/) 98 | - [Debian GNU/Linux](https://tracker.debian.org/pkg/browserpass) 99 | - [macOS](https://github.com/dustinwilson/homebrew-tap/blob/master/browserpass.rb) - make sure to read [these instructions](#installing-browserpass-on-macos-with-homebrew) 100 | - [NixOS](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/security/browserpass/default.nix) - make sure to read [these instructions](#configuring-browserpass-on-nixos--for-nix) 101 | - [Ubuntu](https://launchpad.net/ubuntu/+source/browserpass) 102 | 103 | If your OS is not listed above, proceed with the manual installation steps below. 104 | 105 | #### Download the latest Github release. 106 | 107 | Start out by downloading the [latest release package](https://github.com/dannyvankooten/browserpass/releases) for your operating system. 108 | 109 | #### Verifying authenticity of the releases 110 | 111 | All release files are signed with [this PGP key](https://keybase.io/maximbaz). To verify the signature of a given file, use `$ gpg --verify .sig`. 112 | 113 | It should report: 114 | 115 | ``` 116 | gpg: Signature made ... 117 | gpg: using RSA key 8053EB88879A68CB4873D32B011FDC52DA839335 118 | gpg: Good signature from "Maxim Baz <...>" 119 | gpg: aka ... 120 | Primary key fingerprint: EB4F 9E5A 60D3 2232 BB52 150C 12C8 7A28 FEAC 6B20 121 | Subkey fingerprint: 8053 EB88 879A 68CB 4873 D32B 011F DC52 DA83 9335 122 | ``` 123 | 124 | #### Installing the host application 125 | 126 | 1. Extract the package to where you would like to have the binary. 127 | 1. Run `./install.sh` (`.\install.ps1` on Windows) to install the native messaging host. If you want a system-wide installation, run the script with `sudo`. For Windows, system-wide installation can be done by running `.\install.ps1` as Administrator and specifying "yes" at the "Install for all users?" prompt. 128 | - If you desire a non-interactive installation on a Unix system, pass the name of the browser to the script (e.g. `./install.sh chrome`) 129 | 130 | Installing the binary & registering it with your browser through the installation script is required to allow the browser extension to talk to the local binary application. 131 | 132 | #### Installing the host application on Windows through WSL 133 | 134 | If you already use `pass` under WSL and prefer to have a single copy of your password store, you can use browserpass through WSL as well. 135 | 136 | 1. Install the Windows host application (see previous section) as well as the Linux host application (under WSL). 137 | 2. Create `%localappdata%\browserpass\browserpass-wsl.bat` with the following contents: 138 | 139 | ``` 140 | @echo off 141 | bash -c ~/.browserpass/browserpass-linux64 142 | ``` 143 | 144 | If you installed the Linux host application in a location different from `~/.browserpass`, replace that path in the above script. 145 | 146 | 3. Change the path in `%localappdata%\browserpass\browserpass-firefox.json` (or `-chrome.json`) to point to `browserpass-wsl.bat` 147 | 148 | If your GPG key has a password, the host application running under WSL won't be able to unlock it since it can't interactively prompt for the password. This means you can't decrypt any passwords unless you've already got the key loaded in gpg-agent. 149 | As a workaround, you can use the key (`pass website.com`) in a WSL terminal to load the key into gpg-agent. Then browserpass will work until gpg-agent times out (it is possible to configure larger timeouts, check manual for gpg-agent). 150 | 151 | ### Installing the Chrome extension 152 | 153 | You can either [install the Chrome extension from the Chrome Web Store](https://chrome.google.com/webstore/detail/browserpass/naepdomgkenhinolocfifgehidddafch) or drag the `chrome-browserpass.crx` file from the release package into the [Chrome Extensions](chrome://extensions) (`chrome://extensions`) page. 154 | 155 | ### Installing the Firefox extension 156 | 157 | You can [install the Firefox extension from the Mozilla add-ons site](https://addons.mozilla.org/en-US/firefox/addon/browserpass-ce/). Please note that you will need Firefox 50 or higher. 158 | 159 | ## Updates 160 | 161 | **IMPORTANT**: Majority of the improvements require changing code in both browser extensions and the host application. While we are trying to maintain backwards compatibility, it is expected that you will make sure to keep both components up to date. 162 | 163 | ### Updating the host application 164 | 165 | If you installed the host application via a package manager for your OS, you will likely update it in the the same way. 166 | 167 | If not, repeat the installation instructions for your OS. 168 | 169 | ### Updating browser extensions 170 | 171 | If you installed the extension from a webstore, you will receive updates automatically. 172 | 173 | If not, repeat the installation instructions for the extension. 174 | 175 | ## Usage 176 | 177 | Click the lock icon or use Ctrl+Shift+L to open browserpass with the entries that match current domain. 178 | 179 | - Chrome allows changing the shortcut via chrome://extensions > Keyboard shortcuts. 180 | - Firefox unfortunately does not allow changing the default shortcut. 181 | - Firefox supports the keyboard shortcut only since version 53. 182 | 183 | ### Filter and search modes 184 | 185 | Browserpass has two modes for working with password entries: filter and search. 186 | 187 | When opened, browserpass automatically switches to the filter mode if at least one matching entries exists. 188 | 189 | **Filter mode** is designed to quickly refine a few search results, for example to choose one of several accounts that you have on a given domain. This is done on client side, the filter is always fuzzy and always works in real time. When browserpass is in the filter mode, you will see a domain name in the input field. To exit filter mode, press Backspace. 190 | 191 | **Search mode** is designed to search password entries on your disk, this is much more expensive operation (especially visible on Windows) that's why it is **not** real time, and instead searches only when Enter is pressed. The search is fuzzy by default, but can be changed to glob algorithm in the options. If you want to search everything interactively, just search for `/` or `.` and then use the filter mode to refine the search in real time. 192 | 193 | ### Fill (and submit) the login form 194 | 195 | Click or select the entry that you want to submit, and the login form will be filled with the selected credentials (injected directly into the DOM, browserpass does not use clipboard for this). When the focus is in the input field, hitting Enter will submit the first entry in the list (this is useful in combination with filter mode). 196 | 197 | If the login button is found, it will be focused so that you can just hit Enter to submit the form. If you enable `Automatically submit forms after filling` in the options, the login button will be pressed instead. 198 | 199 | If your password entry has OTP configuration, browserpass will use it at this point to display the code. 200 | 201 | ### Navigating the entries 202 | 203 | Navigate through the list of available credentials with Tab and Shift+Tab or with arrow keys. 204 | 205 | ### Copy to clipboard 206 | 207 | Click on the username or password buttons to copy them to clipboard. Keyboard shortcuts are also available, use Ctrl+C to copy password of the selected entry and Shift+C to copy the username. 208 | 209 | ### Open URL 210 | 211 | Click on the globe button or use the g shortcut to navigate to the URL in the current tab, hold Shift while doing so to open a new tab instead. You can also specify one of the following metadata fields in your pass file to control exactly which URL is navigated to: `url:`, `link:`, `website:`, `web:` or `site:`. 212 | 213 | Keep in mind that browserpass can only fill HTTP basic auth credentials _if you open this URL using browserpass_. 214 | 215 | ### Manual search 216 | 217 | To prevent phishing attacks, browserpass prefills the list of passwords with only those entries that match the current domain. If you want search for credentials across the entire password store, exit the filter mode with Backspace (domain name in the input field will disappear), type the search request and hit Enter to start the search. Instead of using Backspace, you can also type your search query while in the filter mode, as soon as there are no matching results left browserpass will automatically switch to the search mode and will await Enter to initiate the search. 218 | 219 | ### Password store location(s) 220 | 221 | When deciding where to look for the password store, browserpass uses `PASSWORD_STORE_DIR` environment variable, and if it is not defined, checks the `~/.password-store` folder. However, using the `Custom store locations` setting in the options of the browser extension you can configure a different location for browserpass to look for, or even multiple locations. There are no restrictions, you can define subfolders in the password store, gopass mounts or any other folder that has pass entries. 222 | 223 | When you have more than one password store configured and enabled, in order to help you distinguish the password entries from different locations (e.g. between passwords for work and personal GitHub accounts), a green badge next to each password entry will appear indicating its origin (the name of its password store). 224 | 225 | ## Options 226 | 227 | Open settings to configure browserpass: 228 | 229 | - Right click on the lock icon > "Options". 230 | - Find the browserpass in the list of extensions in your browser > "Options". 231 | 232 | The list of currently available options: 233 | 234 | - `Automatically submit forms after filling`: make browserpass automatically submit the login form for you. 235 | - `Use fuzzy search`: whether the _manual search mode_ should be fuzzy or not (filter mode is always fuzzy). 236 | - `Custom store locations`: allows configuring multiple password store locations and toggle them on the fly. 237 | 238 | ## Security 239 | 240 | Browserpass aims to protect your passwords and computer from malicious or fraudulent websites. 241 | 242 | - To protect against phishing, only passwords matching the origin hostname are suggested or selected without an explicit search term. 243 | - To minimize attack surface, the website is not allowed to trigger any extension action without user invocation. 244 | - Only data from the selected password is made available to the website. 245 | - Given full control of the non-native component of the extension, the attacker can extract passwords stored in the configured repository, but can not obtain files elsewhere on the filesystem or reach code execution. 246 | 247 | ## FAQ 248 | 249 | ### Does not work on MacOS: "Native host has exited" 250 | 251 | First install required dependencies: 252 | 253 | ``` 254 | $ brew install gnupg pinentry-mac 255 | ``` 256 | 257 | It is important that you have the `gpg` binary at `/usr/local/bin/gpg`. If you have your `gpg` in another location, create a symlink: 258 | 259 | ``` 260 | $ sudo ln -s /path/to/your/gpg /usr/local/bin/gpg 261 | ``` 262 | 263 | If you don't have admin rights to create the symlink, the workaround is to [patch browser launcher](https://github.com/dannyvankooten/browserpass/issues/197#issuecomment-354586602). 264 | 265 | Now edit `~/.gnupg/gpg.conf`: 266 | 267 | ``` 268 | # Comment out or remove this line if it's there: 269 | # pinentry-mode loopback 270 | 271 | # and add this line: 272 | use-agent 273 | ``` 274 | 275 | Add the following line to `~/.gnupg/gpg-agent.conf`: 276 | 277 | ``` 278 | pinentry-program /usr/local/bin/pinentry-mac 279 | ``` 280 | 281 | Then restart `gpg-agent`: 282 | 283 | ``` 284 | $ gpgconf --kill gpg-agent 285 | ``` 286 | 287 | And finally restart your browser. 288 | 289 | If you still experience the issue, try starting your browser from terminal. If this helps, the issue is likely due to the absence of `/usr/local/bin/gpg`, follow the steps above to make sure it exists. 290 | 291 | ### Configuring Browserpass on NixOS / for Nix 292 | 293 | #### On NixOS 294 | 295 | If you wish to have a stateless setup, make sure you have this in your `/etc/nixos/configuration.nix` and rebuild your system: 296 | 297 | ```nix 298 | { pkgs, ... }: { 299 | programs.browserpass.enable = true; 300 | environment.systemPackages = with pkgs; [ 301 | # All of these browsers will work with it 302 | chromium 303 | firefox 304 | google-chrome 305 | vivaldi 306 | ]; 307 | } 308 | ``` 309 | 310 | Note: firefox*-bin versions do _not_ work statelessly. If you require such firefox versions, use the stateful setup in the following section. 311 | 312 | #### For Nix / stateful 313 | 314 | Install browserpass native messaging host with 315 | 316 | ``` 317 | nix-env -iA nixpkgs.browserpass 318 | ``` 319 | 320 | And install the browser extension like normal. Then link the necessary files 321 | 322 | ``` 323 | # For firefox 324 | mkdir -p ~/.mozilla/native-messaging-hosts && \ 325 | ln -s ~/.nix-profile/lib/mozilla/native-messaging-hosts/com.dannyvankooten.browserpass.json ~/.mozilla/native-messaging-hosts 326 | # For chrome 327 | mkdir -p ~/.config/google-chrome/NativeMessagingHosts && \ 328 | ln -s ~/.nix-profile/etc/chrome-host.json ~/.config/google-chrome/NativeMessagingHosts/com.dannyvankooten.browserpass.json 329 | # For chromium 330 | mkdir -p ~/.config/chromium/NativeMessagingHosts && \ 331 | ln -s ~/.nix-profile/etc/chrome-host.json ~/.config/chromium/NativeMessagingHosts/com.dannyvankooten.browserpass.json 332 | # For vivaldi 333 | mkdir -p ~/.config/vivaldi/NativeMessagingHosts && \ 334 | ln -s ~/.nix-profile/etc/chrome-host.json ~/.config/vivaldi/NativeMessagingHosts/com.dannyvankooten.browserpass.json 335 | ``` 336 | 337 | All versions of firefox are supported with this way 338 | 339 | ### Installing Browserpass on macOS with Homebrew 340 | 341 | Browserpass isn't included in the main Homebrew repository, so it must be installed by adding a third party "tap". That only requires one additional step. 342 | 343 | ``` 344 | $ brew tap dustinwilson/tap 345 | $ brew install browserpass 346 | ``` 347 | 348 | Instead of running `install.sh` Homebrew supplies an additional command called `browserpass-setup` to handle this and works the same way as `install.sh` above. For example this will install the native host files for Firefox: 349 | 350 | ``` 351 | $ browserpass-setup firefox 352 | ``` 353 | 354 | You must install the browser extensions manually using conventional methods for each browser. All of this information is supplied when running `brew install browserpass`. 355 | 356 | ### How to configure OTP? 357 | 358 | The easiest way to add OTP in your password entries is to use [pass-otp](https://github.com/tadfisher/pass-otp). You don't have to configure anything extra, browserpass will automatically detect if an OTP is configured and show you the code after filling the form. 359 | 360 | ## Contributing 361 | 362 | Check out [Contributing](CONTRIBUTING.md) for details on how to build browser extension and host app from sources, and how to load browserpass as an unpacked extension into your browser. 363 | 364 | ## License 365 | 366 | MIT Licensed. 367 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "minimal/xenial64" 3 | config.vm.provision "shell" do |s| 4 | s.inline = %q{ 5 | apt-get update 6 | apt-get install -y nodejs npm golang cmdtest 7 | ln -sfn $(which nodejs) /usr/local/bin/node 8 | 9 | grep -q GOPATH /home/vagrant/.profile || echo 'export GOPATH="${HOME}/go"' >> /home/vagrant/.profile 10 | grep -q browserify /home/vagrant/.profile || echo 'PATH="$PATH:/home/vagrant/go/src/github.com/dannyvankooten/browserpass/node_modules/.bin"' >> /home/vagrant/.profile 11 | mkdir -p go/src/github.com/dannyvankooten/ 12 | ln -sfn /vagrant go/src/github.com/dannyvankooten/browserpass 13 | } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /browserpass.go: -------------------------------------------------------------------------------- 1 | package browserpass 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "encoding/json" 8 | "errors" 9 | "io" 10 | "os/exec" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | 15 | "github.com/dannyvankooten/browserpass/pass" 16 | "github.com/dannyvankooten/browserpass/protector" 17 | "github.com/gokyle/twofactor" 18 | ) 19 | 20 | // Login represents a single pass login. 21 | type Login struct { 22 | Username string `json:"u"` 23 | Password string `json:"p"` 24 | OTP string `json:"digits"` 25 | OTPLabel string `json:"label"` 26 | URL string `json:"url"` 27 | AutoSubmit *bool `json:"autoSubmit,omitempty"` 28 | } 29 | 30 | var endianness = binary.LittleEndian 31 | 32 | // Settings info for the browserpass program. 33 | // 34 | // The browser extension will look up settings in its localstorage and find 35 | // which options have been selected by the user, and put them in a JSON object 36 | // which is then passed along with the command over the native messaging api. 37 | 38 | // Config defines the root config structure sent from the browser extension 39 | type Config struct { 40 | // Manual searches use FuzzySearch if true, GlobSearch otherwise 41 | UseFuzzy bool `json:"use_fuzzy_search"` 42 | CustomStores []pass.StoreDefinition `json:"customStores"` 43 | } 44 | 45 | // msg defines a message sent from a browser extension. 46 | type msg struct { 47 | Settings Config `json:"settings"` 48 | Action string `json:"action"` 49 | Domain string `json:"domain"` 50 | Entry string `json:"entry"` 51 | } 52 | 53 | func SendError(err error, stdout io.Writer) error { 54 | var buf bytes.Buffer 55 | if writeError := json.NewEncoder(&buf).Encode(err.Error()); writeError != nil { 56 | return err 57 | } 58 | if writeError := binary.Write(stdout, endianness, uint32(buf.Len())); writeError != nil { 59 | return err 60 | } 61 | buf.WriteTo(stdout) 62 | return err 63 | } 64 | 65 | // Run starts browserpass. 66 | func Run(stdin io.Reader, stdout io.Writer) error { 67 | protector.Protect("stdio rpath proc exec getpw") 68 | for { 69 | // Get message length, 4 bytes 70 | var n uint32 71 | if err := binary.Read(stdin, endianness, &n); err == io.EOF { 72 | return nil 73 | } else if err != nil { 74 | return SendError(err, stdout) 75 | } 76 | 77 | // Get message body 78 | var data msg 79 | lr := &io.LimitedReader{R: stdin, N: int64(n)} 80 | if err := json.NewDecoder(lr).Decode(&data); err != nil { 81 | return SendError(err, stdout) 82 | } 83 | 84 | s, err := pass.NewDefaultStore(data.Settings.CustomStores, data.Settings.UseFuzzy) 85 | if err != nil { 86 | return SendError(err, stdout) 87 | } 88 | 89 | var resp interface{} 90 | switch data.Action { 91 | case "search": 92 | list, err := s.Search(data.Domain) 93 | if err != nil { 94 | return SendError(err, stdout) 95 | } 96 | resp = list 97 | case "match_domain": 98 | list, err := s.GlobSearch(data.Domain) 99 | if err != nil { 100 | return SendError(err, stdout) 101 | } 102 | resp = list 103 | case "get": 104 | rc, err := s.Open(data.Entry) 105 | if err != nil { 106 | return SendError(err, stdout) 107 | } 108 | defer rc.Close() 109 | login, err := readLoginGPG(rc) 110 | if err != nil { 111 | return SendError(err, stdout) 112 | } 113 | if login.Username == "" { 114 | login.Username = guessUsername(data.Entry) 115 | } 116 | resp = login 117 | default: 118 | return SendError(errors.New("Invalid action"), stdout) 119 | } 120 | 121 | var b bytes.Buffer 122 | if err := json.NewEncoder(&b).Encode(resp); err != nil { 123 | return SendError(err, stdout) 124 | } 125 | 126 | if err := binary.Write(stdout, endianness, uint32(b.Len())); err != nil { 127 | return err 128 | } 129 | if _, err := b.WriteTo(stdout); err != nil { 130 | return err 131 | } 132 | } 133 | } 134 | 135 | func detectGPGBin() (string, error) { 136 | binPriorityList := []string{ 137 | "gpg2", "/bin/gpg2", "/usr/bin/gpg2", "/usr/local/bin/gpg2", 138 | "gpg", "/bin/gpg", "/usr/bin/gpg", "/usr/local/bin/gpg", 139 | } 140 | 141 | binToUse := "" 142 | for _, bin := range binPriorityList { 143 | binCheck := exec.Command(bin, "--version") 144 | if err := binCheck.Run(); err == nil { 145 | binToUse = bin 146 | break 147 | } 148 | } 149 | 150 | if binToUse == "" { 151 | return "", errors.New("Unable to detect the location of gpg binary") 152 | } 153 | 154 | return binToUse, nil 155 | } 156 | 157 | // readLoginGPG reads a encrypted login from r using the system's GPG binary. 158 | func readLoginGPG(r io.Reader) (*Login, error) { 159 | gpgbin, err := detectGPGBin() 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | opts := []string{"--decrypt", "--yes", "--quiet", "--batch", "-"} 165 | 166 | // Run gpg 167 | cmd := exec.Command(gpgbin, opts...) 168 | 169 | cmd.Stdin = r 170 | 171 | rc, err := cmd.StdoutPipe() 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | var errbuf bytes.Buffer 177 | cmd.Stderr = &errbuf 178 | 179 | if err := cmd.Start(); err != nil { 180 | return nil, err 181 | } 182 | 183 | protector.Protect("stdio") 184 | 185 | // Read decrypted output 186 | login, err := parseLogin(rc) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | defer rc.Close() 192 | 193 | if err := cmd.Wait(); err != nil { 194 | return nil, errors.New(err.Error() + "\n" + errbuf.String()) 195 | } 196 | return login, nil 197 | } 198 | 199 | func parseTotp(str string, l *Login) error { 200 | urlPattern := regexp.MustCompile("^otpauth.*$") 201 | ourl := urlPattern.FindString(str) 202 | 203 | if ourl == "" { 204 | tokenPattern := regexp.MustCompile("(?i)^totp(-secret)?:") 205 | token := tokenPattern.ReplaceAllString(str, "") 206 | if len(token) != len(str) { 207 | ourl = "otpauth://totp/?secret=" + strings.TrimSpace(token) 208 | } 209 | } 210 | if ourl != "" { 211 | o, label, err := twofactor.FromURL(ourl) 212 | if err != nil { 213 | return err 214 | } 215 | l.OTP = o.OTP() 216 | l.OTPLabel = label 217 | } 218 | 219 | return nil 220 | } 221 | 222 | // parseLogin parses a login and a password from a decrypted password file. 223 | func parseLogin(r io.Reader) (*Login, error) { 224 | login := new(Login) 225 | 226 | scanner := bufio.NewScanner(r) 227 | 228 | // The first line is the password 229 | scanner.Scan() 230 | login.Password = scanner.Text() 231 | 232 | // Keep reading file for string in "login:", "username:" or "user:" format (case insensitive). 233 | userPattern := regexp.MustCompile("(?i)^(login|username|user):") 234 | urlPattern := regexp.MustCompile("(?i)^(url|link|website|web|site):") 235 | autoSubmitPattern := regexp.MustCompile("(?i)^autosubmit:") 236 | for scanner.Scan() { 237 | line := scanner.Text() 238 | if login.OTP == "" { 239 | parseTotp(line, login) 240 | } 241 | if login.Username == "" { 242 | replaced := userPattern.ReplaceAllString(line, "") 243 | if len(replaced) != len(line) { 244 | login.Username = strings.TrimSpace(replaced) 245 | } 246 | } 247 | if login.URL == "" { 248 | replaced := urlPattern.ReplaceAllString(line, "") 249 | if len(replaced) != len(line) { 250 | login.URL = strings.TrimSpace(replaced) 251 | } 252 | } 253 | if login.AutoSubmit == nil { 254 | replaced := autoSubmitPattern.ReplaceAllString(line, "") 255 | if len(replaced) != len(line) { 256 | value := strings.ToLower(strings.TrimSpace(replaced)) == "true" 257 | login.AutoSubmit = &value 258 | } 259 | } 260 | } 261 | 262 | // if an unlabelled OTP is present, label it with the username 263 | if strings.TrimSpace(login.OTPLabel) == "" && login.OTP != "" { 264 | login.OTPLabel = login.Username 265 | } 266 | 267 | return login, nil 268 | } 269 | 270 | // guessLogin tries to guess a username from an entry's name. 271 | func guessUsername(name string) string { 272 | if strings.Count(filepath.ToSlash(name), "/") >= 1 { 273 | return filepath.Base(name) 274 | } 275 | return "" 276 | } 277 | -------------------------------------------------------------------------------- /browserpass_test.go: -------------------------------------------------------------------------------- 1 | package browserpass 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/gokyle/twofactor" 8 | ) 9 | 10 | func TestParseLogin(t *testing.T) { 11 | r := strings.NewReader("password\n\nfoo\nlogin: bar") 12 | 13 | login, err := parseLogin(r) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | if login.Password != "password" { 19 | t.Errorf("Password is %s, expected %s", login.Password, "password") 20 | } 21 | if login.Username != "bar" { 22 | t.Errorf("Username is %s, expected %s", login.Username, "bar") 23 | } 24 | } 25 | 26 | func TestOtp(t *testing.T) { 27 | r := strings.NewReader("password\n\nfoo\nlogin: bar\notpauth://totp/totp-secret?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=test") 28 | 29 | login, err := parseLogin(r) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | if login.OTPLabel != "totp-secret" { 35 | t.Errorf("OTPLabel is '%s', expected '%s'", login.OTPLabel, "totp-secret") 36 | } 37 | 38 | o, err := twofactor.NewGoogleTOTP("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ") 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | 43 | otp := o.OTP() 44 | 45 | if login.OTP != otp { 46 | t.Errorf("OTP is %s, expected %s", login.OTP, otp) 47 | } 48 | } 49 | 50 | func TestGuessUsername(t *testing.T) { 51 | tests := map[string]string{ 52 | "foo": "", 53 | "foo/bar": "bar", 54 | } 55 | 56 | for input, expected := range tests { 57 | if username := guessUsername(input); username != expected { 58 | t.Errorf("guessUsername(%s): expected %s, got %s", input, expected, username) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /chrome/background.browserify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Tldjs = require("tldjs"); 4 | 5 | var app = "com.dannyvankooten.browserpass"; 6 | 7 | var tabInfos = {}; 8 | var authListeners = {}; 9 | 10 | chrome.runtime.onMessage.addListener(onMessage); 11 | chrome.tabs.onUpdated.addListener(onTabUpdated); 12 | chrome.runtime.onInstalled.addListener(onExtensionInstalled); 13 | 14 | // fill login form & submit 15 | function fillLoginForm(login, tab, autoSubmit) { 16 | const loginParam = JSON.stringify(login); 17 | chrome.tabs.executeScript( 18 | tab.id, 19 | { 20 | allFrames: true, 21 | file: "/inject.js" 22 | }, 23 | function() { 24 | chrome.tabs.executeScript({ 25 | allFrames: true, 26 | code: `browserpassFillForm(${loginParam}, ${autoSubmit});` 27 | }); 28 | } 29 | ); 30 | 31 | if (login.digits) { 32 | tabInfos[tab.id] = { 33 | login: loginParam, 34 | hostname: getHostname(tab.url) 35 | }; 36 | displayOTP(tab.id); 37 | } 38 | } 39 | 40 | function displayOTP(tabId) { 41 | chrome.tabs.executeScript( 42 | tabId, 43 | { 44 | file: "/inject_otp.js" 45 | }, 46 | function() { 47 | chrome.tabs.executeScript(tabId, { 48 | code: `browserpassDisplayOTP(${tabInfos[tabId].login});` 49 | }); 50 | } 51 | ); 52 | } 53 | 54 | function onMessage(request, sender, sendResponse) { 55 | switch (request.action) { 56 | case "login": { 57 | chrome.runtime.sendNativeMessage( 58 | app, 59 | { action: "get", entry: request.entry, settings: getSettings() }, 60 | function(response) { 61 | if (chrome.runtime.lastError) { 62 | var error = chrome.runtime.lastError.message; 63 | console.error(error); 64 | sendResponse({ status: "ERROR", error: error }); 65 | return; 66 | } 67 | 68 | if (typeof response == "string") { 69 | console.error(response); 70 | sendResponse({ status: "ERROR", error: response }); 71 | return; 72 | } 73 | 74 | chrome.tabs.query({ lastFocusedWindow: true, active: true }, function( 75 | tabs 76 | ) { 77 | // do not send login data to page if URL changed during search. 78 | if (tabs[0].url == request.urlDuringSearch) { 79 | var autoSubmit = response.hasOwnProperty("autoSubmit") 80 | ? response.autoSubmit 81 | : getSettings().autoSubmit; 82 | fillLoginForm(response, tabs[0], autoSubmit); 83 | } 84 | }); 85 | 86 | sendResponse({ status: "OK" }); 87 | } 88 | ); 89 | 90 | // Must return true when sendResponse is being called asynchronously 91 | return true; 92 | } 93 | 94 | case "copyToClipboard": { 95 | chrome.runtime.sendNativeMessage( 96 | app, 97 | { action: "get", entry: request.entry, settings: getSettings() }, 98 | function(response) { 99 | if (chrome.runtime.lastError) { 100 | var error = chrome.runtime.lastError.message; 101 | console.error(error); 102 | sendResponse({ status: "ERROR", error: error }); 103 | return; 104 | } 105 | 106 | if (typeof response == "string") { 107 | console.error(response); 108 | sendResponse({ status: "ERROR", error: response }); 109 | return; 110 | } 111 | 112 | let text = ""; 113 | if (request.what === "password") { 114 | text = response.p; 115 | } else if (request.what === "username") { 116 | text = response.u; 117 | } else if (request.what === "otp") { 118 | if (!response.digits) { 119 | sendResponse({ 120 | status: "ERROR", 121 | error: "Unable to determine the OTP code for this entry." 122 | }); 123 | return; 124 | } 125 | text = response.digits; 126 | } 127 | 128 | try { 129 | sendResponse({ status: "OK", text: text }); 130 | } catch (e) { 131 | // If unable to send text to the popup to let it copy the text to clipboard, 132 | // try to copy to clipboard from the background page itself. 133 | // This only works in Chrome. See #241 134 | copyToClipboard(text); 135 | } 136 | } 137 | ); 138 | 139 | // Must return true when sendResponse is being called asynchronously 140 | return true; 141 | } 142 | 143 | case "dismissOTP": { 144 | if (request.action == "dismissOTP" && sender.tab.id in tabInfos) { 145 | delete tabInfos[sender.tab.id]; 146 | } 147 | break; 148 | } 149 | 150 | // allows the local communication to request settings. Returns an 151 | // object that has current settings. Update this as new settings 152 | // are added (or old ones removed) 153 | case "getSettings": { 154 | sendResponse(getSettings()); 155 | break; 156 | } 157 | 158 | // spawn a new tab with credentials from the password file 159 | case "launch": { 160 | chrome.runtime.sendNativeMessage( 161 | app, 162 | { action: "get", entry: request.entry, settings: getSettings() }, 163 | function(response) { 164 | if (chrome.runtime.lastError) { 165 | var error = chrome.runtime.lastError.message; 166 | console.error(error); 167 | sendResponse({ status: "ERROR", error: error }); 168 | return; 169 | } 170 | 171 | if (typeof response == "string") { 172 | console.error(response); 173 | sendResponse({ status: "ERROR", error: response }); 174 | return; 175 | } 176 | 177 | if (!response.hasOwnProperty("url") || response.url.length == 0) { 178 | // guess url from login path if not available in the host app response 179 | response.url = parseUrlFromEntry(request.entry); 180 | 181 | // if url is not available at this point, send an error 182 | if (!response.hasOwnProperty("url") || response.url.length == 0) { 183 | sendResponse({ 184 | status: "ERROR", 185 | error: 186 | "Unable to determine the URL for this entry. If you have defined one in the password file, " + 187 | "your host application must be at least v2.0.14 for this to be usable." 188 | }); 189 | return; 190 | } 191 | } 192 | 193 | var url = response.url.match(/^([a-z]+:)?\/\//i) 194 | ? response.url 195 | : "http://" + response.url; 196 | 197 | var tabsMethod = request.openInNewTab ? "create" : "update"; 198 | chrome.tabs[tabsMethod]({ url: url }, function(tab) { 199 | var authAttempted = false; 200 | 201 | authListeners[tab.id] = function(requestDetails) { 202 | // only supply credentials if this is the first time for this tab, and the tab is not loaded 203 | if (authAttempted) { 204 | return {}; 205 | } 206 | authAttempted = true; 207 | return onAuthRequired(url, requestDetails, response); 208 | }; 209 | 210 | // intercept requests for authentication 211 | chrome.webRequest.onAuthRequired.addListener( 212 | authListeners[tab.id], 213 | { urls: ["*://*/*"], tabId: tab.id }, 214 | ["blocking"] 215 | ); 216 | }); 217 | 218 | sendResponse({ status: "OK" }); 219 | } 220 | ); 221 | 222 | // Must return true when sendResponse is being called asynchronously 223 | return true; 224 | } 225 | } 226 | } 227 | 228 | function parseUrlFromEntry(entry) { 229 | var parts = 230 | entry.indexOf(":") > 0 ? entry.substr(entry.indexOf(":") + 1) : entry; 231 | parts = parts.split(/\//).reverse(); 232 | for (var i in parts) { 233 | var part = parts[i]; 234 | var info = Tldjs.parse(part); 235 | if ( 236 | info.isValid && 237 | info.tldExists && 238 | info.domain !== null && 239 | info.hostname === part 240 | ) { 241 | return part; 242 | } 243 | } 244 | return ""; 245 | } 246 | 247 | function copyToClipboard(s) { 248 | var clipboard = document.createElement("input"); 249 | document.body.appendChild(clipboard); 250 | clipboard.value = s; 251 | clipboard.select(); 252 | document.execCommand("copy"); 253 | clipboard.blur(); 254 | document.body.removeChild(clipboard); 255 | } 256 | 257 | function getSettings() { 258 | // default settings 259 | var settings = { 260 | autoSubmit: false, 261 | use_fuzzy_search: true, 262 | customStores: [] 263 | }; 264 | 265 | // load settings from local storage 266 | for (var key in settings) { 267 | var value = localStorage.getItem(key); 268 | if (value !== null) { 269 | settings[key] = JSON.parse(value); 270 | } 271 | } 272 | 273 | // filter custom stores by enabled & path length, and ensure they are named 274 | settings.customStores = settings.customStores 275 | .filter(store => store.enabled && store.path.length > 0) 276 | .map(function(store) { 277 | if (!store.name) { 278 | store.name = store.path; 279 | } 280 | return store; 281 | }); 282 | 283 | return settings; 284 | } 285 | 286 | // listener function for authentication interception 287 | function onAuthRequired(url, requestDetails, response) { 288 | // ask the user before sending credentials to a different domain 289 | var launchHost = url.match(/:\/\/([^\/]+)/)[1]; 290 | if (launchHost !== requestDetails.challenger.host) { 291 | var message = 292 | "You are about to send login credentials to a domain that is different than " + 293 | "the one you lauched from the browserpass extension. Do you wish to proceed?\n\n" + 294 | "Launched URL: " + 295 | url + 296 | "\n" + 297 | "Authentication URL: " + 298 | requestDetails.url; 299 | if (!confirm(message)) { 300 | return {}; 301 | } 302 | } 303 | 304 | // ask the user before sending credentials over an insecure connection 305 | if (!requestDetails.url.match(/^https:/i)) { 306 | var message = 307 | "You are about to send login credentials via an insecure connection!\n\n" + 308 | "Are you sure you want to do this? If there is an attacker watching your " + 309 | "network traffic, they may be able to see your username and password.\n\n" + 310 | "URL: " + 311 | requestDetails.url; 312 | if (!confirm(message)) { 313 | return {}; 314 | } 315 | } 316 | 317 | // supply credentials 318 | return { 319 | authCredentials: { 320 | username: response.u, 321 | password: response.p 322 | } 323 | }; 324 | } 325 | 326 | function onTabUpdated(tabId, info, tab) { 327 | if (info.url && tabId in tabInfos) { 328 | if (getHostname(info.url) != tabInfos[tabId].hostname) { 329 | delete tabInfos[tabId]; 330 | } 331 | } 332 | 333 | if (info.status != "complete") { 334 | return; 335 | } 336 | 337 | if (tabId in tabInfos) { 338 | displayOTP(tabId); 339 | } 340 | 341 | if (tabId in authListeners) { 342 | chrome.webRequest.onAuthRequired.removeListener(authListeners[tabId]); 343 | delete authListeners[tabId]; 344 | } 345 | } 346 | 347 | function getHostname(url) { 348 | // Manipulate the browser into parsing the URL for us 349 | var a = document.createElement("a"); 350 | a.href = url; 351 | return a.hostname; 352 | } 353 | 354 | function onExtensionInstalled(details) { 355 | // No permissions 356 | if (!chrome.notifications) { 357 | return; 358 | } 359 | 360 | if (details.reason != "update") { 361 | return; 362 | } 363 | 364 | var changelog = { 365 | 2012: "Breaking change: please update the host app to at least v2.0.12", 366 | 2023: "New major version will be released on Sat, 13 April. Plan for maintenance, Browserpass will stop working until you update native host app to v3! For more info, see README in https://github.com/browserpass/browserpass" 367 | }; 368 | 369 | var parseVersion = version => parseInt(version.replace(/\./g, "")); 370 | var newVersion = parseVersion(chrome.runtime.getManifest().version); 371 | var prevVersion = parseVersion(details.previousVersion); 372 | 373 | Object.keys(changelog) 374 | .sort() 375 | .forEach(function(version) { 376 | if (version > prevVersion && version <= newVersion) { 377 | chrome.notifications.create(version, { 378 | title: "browserpass: Important changes", 379 | message: changelog[version], 380 | iconUrl: "icon-lock.png", 381 | type: "basic" 382 | }); 383 | } 384 | }); 385 | } 386 | -------------------------------------------------------------------------------- /chrome/content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Browserpass 6 | 7 | 8 | 9 |
10 |
A JavaScript error occured.
11 |
12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /chrome/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.dannyvankooten.browserpass", 3 | "description": "Browserpass binary for the Chrome extension", 4 | "path": "%%replace%%", 5 | "type": "stdio", 6 | "allowed_origins": [ 7 | "chrome-extension://naepdomgkenhinolocfifgehidddafch/", 8 | "chrome-extension://klfoddkbhleoaabpmiigbmpbjfljimgb/" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /chrome/icon-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /chrome/icon-globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /chrome/icon-key.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chrome/icon-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/chrome/icon-lock.png -------------------------------------------------------------------------------- /chrome/icon-lock.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chrome/icon-otp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /chrome/icon-search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chrome/icon-user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /chrome/inject.js: -------------------------------------------------------------------------------- 1 | window.browserpassFillForm = function(login, autoSubmit) { 2 | const FORM_MARKERS = [ 3 | "login", 4 | "log-in", 5 | "log_in", 6 | "signin", 7 | "sign-in", 8 | "sign_in" 9 | ]; 10 | const USERNAME_FIELDS = { 11 | selectors: [ 12 | "input[id*=openid i]", 13 | "input[name*=openid i]", 14 | "input[class*=openid i]", 15 | "input[id*=user i]", 16 | "input[name*=user i]", 17 | "input[class*=user i]", 18 | "input[id*=login i]", 19 | "input[name*=login i]", 20 | "input[class*=login i]", 21 | "input[id*=email i]", 22 | "input[name*=email i]", 23 | "input[class*=email i]", 24 | "input[type=email i]", 25 | "input[type=text i]", 26 | "input[type=tel i]" 27 | ], 28 | types: ["email", "text", "tel"] 29 | }; 30 | const PASSWORD_FIELDS = { 31 | selectors: ["input[type=password i]"] 32 | }; 33 | const INPUT_FIELDS = { 34 | selectors: PASSWORD_FIELDS.selectors.concat(USERNAME_FIELDS.selectors) 35 | }; 36 | const SUBMIT_FIELDS = { 37 | selectors: [ 38 | "[type=submit i]", 39 | "button[name*=login i]", 40 | "button[name*=log-in i]", 41 | "button[name*=log_in i]", 42 | "button[name*=signin i]", 43 | "button[name*=sign-in i]", 44 | "button[name*=sign_in i]", 45 | "button[id*=login i]", 46 | "button[id*=log-in i]", 47 | "button[id*=log_in i]", 48 | "button[id*=signin i]", 49 | "button[id*=sign-in i]", 50 | "button[id*=sign_in i]", 51 | "button[class*=login i]", 52 | "button[class*=log-in i]", 53 | "button[class*=log_in i]", 54 | "button[class*=signin i]", 55 | "button[class*=sign-in i]", 56 | "button[class*=sign_in i]", 57 | "input[type=button i][name*=login i]", 58 | "input[type=button i][name*=log-in i]", 59 | "input[type=button i][name*=log_in i]", 60 | "input[type=button i][name*=signin i]", 61 | "input[type=button i][name*=sign-in i]", 62 | "input[type=button i][name*=sign_in i]", 63 | "input[type=button i][id*=login i]", 64 | "input[type=button i][id*=log-in i]", 65 | "input[type=button i][id*=log_in i]", 66 | "input[type=button i][id*=signin i]", 67 | "input[type=button i][id*=sign-in i]", 68 | "input[type=button i][id*=sign_in i]", 69 | "input[type=button i][class*=login i]", 70 | "input[type=button i][class*=log-in i]", 71 | "input[type=button i][class*=log_in i]", 72 | "input[type=button i][class*=signin i]", 73 | "input[type=button i][class*=sign-in i]", 74 | "input[type=button i][class*=sign_in i]" 75 | ] 76 | }; 77 | 78 | function queryAllVisible(parent, field, form) { 79 | var result = []; 80 | for (var i = 0; i < field.selectors.length; i++) { 81 | var elems = parent.querySelectorAll(field.selectors[i]); 82 | for (var j = 0; j < elems.length; j++) { 83 | var elem = elems[j]; 84 | // Select only elements from specified form 85 | if (form && form != elem.form) { 86 | continue; 87 | } 88 | // Ignore disabled fields 89 | if (elem.disabled) { 90 | continue; 91 | } 92 | // Elem or its parent has a style 'display: none', 93 | // or it is just too narrow to be a real field (a trap for spammers?). 94 | if (elem.offsetWidth < 30 || elem.offsetHeight < 10) { 95 | continue; 96 | } 97 | // We may have a whitelist of acceptable field types. If so, skip elements of a different type. 98 | if (field.types && field.types.indexOf(elem.type.toLowerCase()) < 0) { 99 | continue; 100 | } 101 | // Elem takes space on the screen, but it or its parent is hidden with a visibility style. 102 | var style = window.getComputedStyle(elem); 103 | if (style.visibility == "hidden") { 104 | continue; 105 | } 106 | // Elem is outside of the boundaries of the visible viewport. 107 | var rect = elem.getBoundingClientRect(); 108 | if ( 109 | rect.x + rect.width < 0 || 110 | rect.y + rect.height < 0 || 111 | (rect.x > window.innerWidth || rect.y > window.innerHeight) 112 | ) { 113 | continue; 114 | } 115 | // This element is visible, will use it. 116 | result.push(elem); 117 | } 118 | } 119 | return result; 120 | } 121 | 122 | function queryFirstVisible(parent, field, form) { 123 | var elems = queryAllVisible(parent, field, form); 124 | return elems.length > 0 ? elems[0] : undefined; 125 | } 126 | 127 | function form() { 128 | var elems = queryAllVisible(document, INPUT_FIELDS, undefined); 129 | var forms = []; 130 | for (var i = 0; i < elems.length; i++) { 131 | var form = elems[i].form; 132 | if (form && forms.indexOf(form) < 0) { 133 | forms.push(form); 134 | } 135 | } 136 | if (forms.length == 0) { 137 | return undefined; 138 | } 139 | if (forms.length == 1) { 140 | return forms[0]; 141 | } 142 | 143 | // If there are multiple forms, try to detect which one is a login form 144 | var formProps = []; 145 | for (var i = 0; i < forms.length; i++) { 146 | var form = forms[i]; 147 | var props = [form.id, form.name, form.className]; 148 | formProps.push(props); 149 | for (var j = 0; j < FORM_MARKERS.length; j++) { 150 | var marker = FORM_MARKERS[j]; 151 | for (var k = 0; k < props.length; k++) { 152 | var prop = props[k]; 153 | if (prop.toLowerCase().indexOf(marker) > -1) { 154 | return form; 155 | } 156 | } 157 | } 158 | } 159 | 160 | console.error( 161 | "Unable to detect which of the multiple available forms is the login form. Please submit an issue for browserpass on github, and provide the following list in the details: " + 162 | JSON.stringify(formProps) 163 | ); 164 | return forms[0]; 165 | } 166 | 167 | function find(field, form) { 168 | return queryFirstVisible(document, field, form); 169 | } 170 | 171 | function update(field, value, form) { 172 | if (!value.length) { 173 | return false; 174 | } 175 | 176 | // Focus the input element first 177 | var el = find(field, form); 178 | if (!el) { 179 | return false; 180 | } 181 | var eventNames = ["click", "focus"]; 182 | eventNames.forEach(function(eventName) { 183 | el.dispatchEvent(new Event(eventName, { bubbles: true })); 184 | }); 185 | 186 | // Focus may have triggered unvealing a true input, find it again 187 | el = find(field, form); 188 | if (!el) { 189 | return false; 190 | } 191 | 192 | // Now set the value and unfocus 193 | el.setAttribute("value", value); 194 | el.value = value; 195 | var eventNames = [ 196 | "keypress", 197 | "keydown", 198 | "keyup", 199 | "input", 200 | "blur", 201 | "change" 202 | ]; 203 | eventNames.forEach(function(eventName) { 204 | el.dispatchEvent(new Event(eventName, { bubbles: true })); 205 | }); 206 | return true; 207 | } 208 | 209 | var loginForm = form(); 210 | 211 | update(USERNAME_FIELDS, login.u, loginForm); 212 | update(PASSWORD_FIELDS, login.p, loginForm); 213 | 214 | var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, loginForm); 215 | if (password_inputs.length > 1) { 216 | // There is likely a field asking for OTP code, so do not submit form just yet 217 | password_inputs[1].select(); 218 | } else { 219 | window.requestAnimationFrame(function() { 220 | // Try to submit the form, or focus on the submit button (based on user settings) 221 | var submit = find(SUBMIT_FIELDS, loginForm); 222 | if (submit) { 223 | if (autoSubmit) { 224 | submit.click(); 225 | } else { 226 | submit.focus(); 227 | } 228 | } else { 229 | // There is no submit button. Try to submit the form itself. 230 | if (autoSubmit && loginForm) { 231 | loginForm.submit(); 232 | } 233 | // We need to keep focus somewhere within the form, so that Enter hopefully submits the form. 234 | var password = find(PASSWORD_FIELDS, loginForm); 235 | if (password) { 236 | password.focus(); 237 | } else { 238 | var username = find(USERNAME_FIELDS, loginForm); 239 | if (username) { 240 | username.focus(); 241 | } 242 | } 243 | } 244 | }); 245 | } 246 | }; 247 | -------------------------------------------------------------------------------- /chrome/inject_otp.js: -------------------------------------------------------------------------------- 1 | window.browserpassDisplayOTP = function(login) { 2 | iframe = document.createElement("iframe"); 3 | iframe.id = "browserpass-otp-iframe"; 4 | iframe.src = chrome.runtime.getURL("otp.html"); 5 | iframe.scrolling = "no"; 6 | iframe.style = ` 7 | position: fixed; 8 | top: 0; 9 | right: 0; 10 | background-color: white; 11 | border-bottom-left-radius: 4px; 12 | border-left: 1px solid #888888; 13 | border-bottom: 1px solid #888888; 14 | border-top: none; 15 | border-right: none; 16 | box-sizing: content-box; 17 | z-index: 1000000; 18 | overflow: hidden; 19 | visibility: hidden; 20 | `; 21 | 22 | window.addEventListener("message", receiveMessage, false); 23 | function receiveMessage(event) { 24 | if (event.data.action == "load") { 25 | iframe.contentWindow.postMessage(login, "*"); 26 | } 27 | 28 | if (event.data.action == "resize") { 29 | iframe.style.visibility = "visible"; 30 | 31 | iframe.width = event.data.payload.width; 32 | iframe.height = event.data.payload.height; 33 | } 34 | 35 | if (event.data.action == "dismiss") { 36 | iframe.remove(); 37 | window.removeEventListener("message", receiveMessage); 38 | } 39 | } 40 | 41 | var oldIframe = document.getElementById("browserpass-otp-iframe"); 42 | if (oldIframe != null) { 43 | oldIframe.remove(); 44 | } 45 | document.body.appendChild(iframe); 46 | }; 47 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvlVUvevBvdeIFpvK5Xjcbd/cV8AsMNLg0Y7BmUetSTagjts949Tp12mNmWmIEEaE9Zwmfjl1ownWiclGhsoPSf6x7nP/i0j8yROv6TYibXLhZet9y4vnUMgtCIkb3O5RnuOl0Y+V3XUADwxotmgT1laPUThymJoYnWPv+lwDkYiEopX2Aq2amzRj8aMogNBUbAIkCMxfa9WK3Vm0QTAUdV4ii9WqzbgjHVruQpiFVq99W2U9ddsWNZjOG/36sFREuHw+reulQgblp9FZdaN1Q9X5cGcT5bncQIRB6K3wZYa805gFENc93Wslmzu6aUSEKqqPymlI5ikedaPlXPmlqwIDAQAB", 3 | "manifest_version": 2, 4 | "name": "browserpass-ce", 5 | "description": "Chrome extension for zx2c4's pass (password manager) - Community Edition.", 6 | "version": "2.0.23", 7 | "author": "Danny van Kooten", 8 | "homepage_url": "https://github.com/dannyvankooten/browserpass", 9 | "background": { 10 | "persistent": true, 11 | "scripts": [ 12 | "background.js" 13 | ] 14 | }, 15 | "options_page": "options.html", 16 | "options_ui": { 17 | "page": "options.html", 18 | "chrome_style": true, 19 | "open_in_tab": false 20 | }, 21 | "browser_action": { 22 | "default_icon": "icon-lock.png", 23 | "default_popup": "content.html" 24 | }, 25 | "permissions": [ 26 | "tabs", 27 | "activeTab", 28 | "nativeMessaging", 29 | "notifications", 30 | "storage", 31 | "webRequest", 32 | "webRequestBlocking", 33 | "http://*/*", 34 | "https://*/*" 35 | ], 36 | "commands": { 37 | "_execute_browser_action": { 38 | "suggested_key": { 39 | "default": "Ctrl+Shift+L" 40 | } 41 | } 42 | }, 43 | "web_accessible_resources": [ 44 | "otp.html" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /chrome/options.browserify.js: -------------------------------------------------------------------------------- 1 | var settings = { 2 | autoSubmit: { 3 | type: "checkbox", 4 | title: "Automatically submit forms after filling", 5 | value: false 6 | }, 7 | use_fuzzy_search: { 8 | type: "checkbox", 9 | title: "Use fuzzy search", 10 | value: true 11 | }, 12 | customStores: { 13 | title: "Custom password store locations", 14 | value: [{ enabled: true, name: "", path: "" }] 15 | } 16 | }; 17 | 18 | // load settings & create render tree 19 | loadSettings(); 20 | var tree = { 21 | view: function() { 22 | var nodes = [m("h3", "Basic Settings")]; 23 | for (var key in settings) { 24 | var type = settings[key].type; 25 | if (type == "checkbox") { 26 | nodes.push(createCheckbox(key, settings[key])); 27 | } 28 | } 29 | nodes.push(m("h3", "Custom Store Locations")); 30 | for (var key in settings.customStores.value) { 31 | nodes.push(createCustomStore(key, settings.customStores.value[key])); 32 | } 33 | nodes.push( 34 | m( 35 | "button.add-store", 36 | { 37 | onclick: function() { 38 | settings.customStores.value.push({ 39 | enabled: true, 40 | name: "", 41 | path: "" 42 | }); 43 | saveSetting("customStores"); 44 | } 45 | }, 46 | "Add Store" 47 | ) 48 | ); 49 | return nodes; 50 | } 51 | }; 52 | 53 | // attach tree 54 | var m = require("mithril"); 55 | m.mount(document.body, tree); 56 | 57 | // load settings from local storage 58 | function loadSettings() { 59 | for (var key in settings) { 60 | var value = localStorage.getItem(key); 61 | if (value !== null) { 62 | settings[key].value = JSON.parse(value); 63 | } 64 | } 65 | } 66 | 67 | // save settings to local storage 68 | function saveSetting(name) { 69 | var value = settings[name].value; 70 | if (Array.isArray(value)) { 71 | value = value.filter(item => item !== null); 72 | } 73 | value = JSON.stringify(value); 74 | localStorage.setItem(name, value); 75 | } 76 | 77 | // create a checkbox option 78 | function createCheckbox(name, option) { 79 | return m("div.option", { class: name }, [ 80 | m("input[type=checkbox]", { 81 | name: name, 82 | title: option.title, 83 | checked: option.value, 84 | onchange: function(e) { 85 | settings[name].value = e.target.checked; 86 | saveSetting(name); 87 | } 88 | }), 89 | m("label", { for: name }, option.title) 90 | ]); 91 | } 92 | 93 | // create a custom store option 94 | function createCustomStore(key, store) { 95 | return m("div.option.custom-store", { class: "store-" + store.name }, [ 96 | m("input[type=checkbox]", { 97 | title: "Whether to enable this password store", 98 | checked: store.enabled, 99 | onchange: function(e) { 100 | store.enabled = e.target.checked; 101 | saveSetting("customStores"); 102 | } 103 | }), 104 | m("input[type=text].name", { 105 | title: "The name for this password store", 106 | value: store.name, 107 | placeholder: "name", 108 | onchange: function(e) { 109 | store.name = e.target.value; 110 | saveSetting("customStores"); 111 | } 112 | }), 113 | m("input[type=text].path", { 114 | title: "The full path to this password store", 115 | value: store.path, 116 | placeholder: "/path/to/store", 117 | onchange: function(e) { 118 | store.path = e.target.value; 119 | saveSetting("customStores"); 120 | } 121 | }), 122 | m( 123 | "a.remove", 124 | { 125 | title: "Remove this password store", 126 | onclick: function() { 127 | delete settings.customStores.value[key]; 128 | saveSetting("customStores"); 129 | } 130 | }, 131 | "[X]" 132 | ) 133 | ]); 134 | } 135 | -------------------------------------------------------------------------------- /chrome/options.css: -------------------------------------------------------------------------------- 1 | .option { 2 | display: flex; 3 | height: 16px; 4 | line-height: 16px; 5 | margin-bottom: 8px; 6 | } 7 | 8 | .option input[type="checkbox"] { 9 | height: 12px; 10 | margin: 2px 6px 2px 0; 11 | padding: 0; 12 | } 13 | 14 | .option.custom-store input[type="text"] { 15 | background-color: white; 16 | color: black; 17 | border: none; 18 | border-bottom: 1px solid #aaa; 19 | height: 16px; 20 | line-height: 16px; 21 | margin: -4px 0 0 0; 22 | overflow: hidden; 23 | padding: 0; 24 | width: 25%; 25 | } 26 | 27 | .option.custom-store input[type="text"].path { 28 | margin-left: 6px; 29 | width: calc(100% - 25% - 48px); 30 | } 31 | 32 | .option.custom-store a.remove { 33 | color: #f00; 34 | display: block; 35 | height: 16px; 36 | line-height: 16px; 37 | margin: 0 0 0 6px; 38 | padding: 0; 39 | text-decoration: none; 40 | width: 16px; 41 | } 42 | 43 | .add-store { 44 | margin-top: 8px; 45 | } 46 | 47 | @-moz-document url-prefix() { 48 | html, 49 | body { 50 | box-sizing: border-box; 51 | overflow: hidden; 52 | } 53 | 54 | body { 55 | background: #fff; 56 | border: 1px solid #000; 57 | font-family: sans; 58 | margin: 2px; 59 | padding: 12px; 60 | } 61 | 62 | .option.custom-store input[type="text"] { 63 | background: #fff; 64 | margin: 2px; 65 | } 66 | 67 | .option.custom-store a.remove { 68 | font-size: 12px; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /chrome/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Browserpass Chrome extension for zx2c4's pass (password manager) 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /chrome/otp.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 8px; 4 | width: max-content; 5 | width: -moz-max-content; 6 | font-family: monospace; 7 | font-size: 1em; 8 | white-space: nowrap; 9 | } 10 | 11 | button { 12 | background: #eeeeee; 13 | color: black; 14 | border: 1px solid #888888; 15 | border-radius: 2px; 16 | padding: 1px 6px; 17 | } 18 | 19 | button:focus { 20 | background: #cccccc; 21 | } 22 | 23 | #otp { 24 | text-align: center; 25 | border: 1px solid #888888; 26 | border-radius: 2px; 27 | background: white; 28 | color: black; 29 | } 30 | 31 | #copy-icon { 32 | height: 1em; 33 | vertical-align: middle; 34 | } 35 | -------------------------------------------------------------------------------- /chrome/otp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /chrome/otp.js: -------------------------------------------------------------------------------- 1 | var otpInput = document.getElementById("otp"); 2 | var otpLabel = document.getElementById("label"); 3 | var otpCopy = document.getElementById("copy"); 4 | var otpDismiss = document.getElementById("dismiss"); 5 | 6 | window.addEventListener("message", receiveMessage, false); 7 | function receiveMessage(event) { 8 | otpInput.value = event.data.digits; 9 | otpInput.setAttribute("size", event.data.digits.length); 10 | otpLabel.innerText = (event.data.label || "OTP") + ":"; 11 | var message = { 12 | action: "resize", 13 | payload: { 14 | width: document.body.scrollWidth, 15 | height: document.body.scrollHeight 16 | } 17 | }; 18 | window.parent.postMessage(message, "*"); 19 | } 20 | 21 | window.onload = function() { 22 | window.parent.postMessage({ action: "load" }, "*"); 23 | }; 24 | 25 | document.body.onclick = function() { 26 | otpInput.select(); 27 | }; 28 | 29 | otpCopy.onclick = function() { 30 | otpInput.select(); 31 | document.execCommand("copy"); 32 | }; 33 | 34 | otpDismiss.onclick = function() { 35 | chrome.runtime.sendMessage({ action: "dismissOTP" }); 36 | window.parent.postMessage({ action: "dismiss" }, "*"); 37 | }; 38 | -------------------------------------------------------------------------------- /chrome/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExtensionInstallForcelist": [ 3 | "naepdomgkenhinolocfifgehidddafch;https://clients2.google.com/service/update2/crx" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /chrome/script.browserify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var m = require("mithril"); 4 | var FuzzySort = require("fuzzysort"); 5 | var app = "com.dannyvankooten.browserpass"; 6 | var activeTab; 7 | var searching = false; 8 | var resultLogins = []; 9 | var logins = []; 10 | var fillOnSubmit = false; 11 | var error; 12 | var domain, urlDuringSearch; 13 | var searchSettings; 14 | 15 | // load settings and initialise popup 16 | chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) { 17 | searchSettings = settings; 18 | 19 | m.mount(document.getElementById("mount"), { view: view, oncreate: oncreate }); 20 | 21 | chrome.tabs.onActivated.addListener(init); 22 | chrome.tabs.query({ lastFocusedWindow: true, active: true }, function(tabs) { 23 | init(tabs[0]); 24 | }); 25 | }); 26 | 27 | function view() { 28 | var results = ""; 29 | 30 | if (searching) { 31 | results = m("div.loader"); 32 | } else if (error) { 33 | results = m("div.status-text", "Error: " + error); 34 | error = undefined; 35 | } else if (logins) { 36 | if (logins.length === 0 && domain && domain.length > 0) { 37 | results = m( 38 | "div.status-text", 39 | m.trust(`No matching passwords found for ${domain}.`) 40 | ); 41 | } else if (logins.length > 0) { 42 | results = logins.map(function(login) { 43 | let selector = "button.login"; 44 | let options = { 45 | onclick: getLoginData.bind(login), 46 | title: 47 | "Fill form" + 48 | (searchSettings && searchSettings.autoSubmit ? " and submit" : "") 49 | }; 50 | 51 | var store = "default"; 52 | var storeTitle = "Default password store"; 53 | var name = login; 54 | var loginStoreSplitterIndex = login.indexOf(":"); 55 | if (loginStoreSplitterIndex > -1) { 56 | if (searchSettings && searchSettings.customStores.length > 1) { 57 | store = login.substr(0, loginStoreSplitterIndex); 58 | for (let i = 0; i < searchSettings.customStores.length; i++) { 59 | let customStore = searchSettings.customStores[i]; 60 | if (customStore.name == store) { 61 | storeTitle = customStore.path; 62 | break; 63 | } 64 | } 65 | } 66 | name = login.substr(loginStoreSplitterIndex + 1); 67 | } 68 | 69 | let faviconUrl = getFaviconUrl(domain); 70 | if (faviconUrl) { 71 | selector += ".favicon"; 72 | options.style = `background-image: url('${faviconUrl}')`; 73 | } 74 | 75 | return m("div.entry", [ 76 | m(selector, options, [ 77 | loginStoreSplitterIndex > -1 && store != "default" 78 | ? m("div.store", { title: storeTitle }, store) 79 | : null, 80 | m("div.name", name) 81 | ]), 82 | m("button.launch.url", { 83 | onclick: launchURL.bind({ entry: login }), 84 | title: "Visit URL", 85 | tabindex: -1 86 | }), 87 | m("button.copy.username", { 88 | onclick: loginToClipboard.bind({ entry: login, what: "username" }), 89 | title: "Copy username", 90 | tabindex: -1 91 | }), 92 | m("button.copy.password", { 93 | onclick: loginToClipboard.bind({ entry: login, what: "password" }), 94 | title: "Copy password", 95 | tabindex: -1 96 | }), 97 | m("button.copy.otp", { 98 | onclick: loginToClipboard.bind({ entry: login, what: "otp" }), 99 | title: "Copy OTP code", 100 | tabindex: -1 101 | }) 102 | ]); 103 | }); 104 | } 105 | } 106 | 107 | return m("div.container", { onkeydown: keyHandler }, [ 108 | // search form 109 | m("div.search", [ 110 | m( 111 | "form", 112 | { 113 | onsubmit: submitSearchForm, 114 | onkeydown: searchKeyHandler 115 | }, 116 | [ 117 | m("div", { 118 | id: "filter-search" 119 | }), 120 | m("div", [ 121 | m("input", { 122 | type: "text", 123 | id: "search-field", 124 | name: "s", 125 | placeholder: "Search passwords...", 126 | autocomplete: "off", 127 | autofocus: "on", 128 | oninput: filterLogins 129 | }), 130 | m("input", { 131 | type: "submit", 132 | value: "Search", 133 | style: "display: none;" 134 | }) 135 | ]) 136 | ] 137 | ) 138 | ]), 139 | 140 | // results 141 | m("div.results", results) 142 | ]); 143 | } 144 | 145 | function filterLogins(e) { 146 | // use fuzzy search to filter results 147 | var filter = e.target.value.trim().split(/[\s\/]+/); 148 | if (filter.length > 0) { 149 | logins = resultLogins.slice(0); 150 | filter.forEach(function(word) { 151 | if (word.length > 0) { 152 | var refine = []; 153 | FuzzySort.go(word, logins, { allowTypo: false }).forEach(function( 154 | result 155 | ) { 156 | refine.push(result.target); 157 | }); 158 | logins = refine.slice(0); 159 | } 160 | }); 161 | 162 | // fill login forms on submit rather than initiating a search 163 | fillOnSubmit = logins.length > 0; 164 | } else { 165 | // reset the result list if the filter is empty 166 | logins = resultLogins.slice(0); 167 | } 168 | 169 | // redraw the list 170 | m.redraw(); 171 | 172 | // show / hide the filter hint 173 | showFilterHint(logins.length); 174 | } 175 | 176 | function searchKeyHandler(e) { 177 | // switch to search mode if backspace is pressed and no filter text has been entered 178 | if ( 179 | e.code == "Backspace" && 180 | logins.length > 0 && 181 | e.target.value.length == 0 182 | ) { 183 | e.preventDefault(); 184 | logins = resultLogins = []; 185 | e.target.value = fillOnSubmit ? "" : domain; 186 | domain = ""; 187 | showFilterHint(false); 188 | } 189 | } 190 | 191 | function showFilterHint(show = true) { 192 | var filterHint = document.getElementById("filter-search"); 193 | var searchField = document.getElementById("search-field"); 194 | if (show) { 195 | filterHint.style.display = "block"; 196 | searchField.setAttribute("placeholder", "Refine search..."); 197 | } else { 198 | filterHint.style.display = "none"; 199 | searchField.setAttribute("placeholder", "Search passwords..."); 200 | } 201 | } 202 | 203 | function submitSearchForm(e) { 204 | e.preventDefault(); 205 | if (fillOnSubmit && logins.length > 0) { 206 | // fill using the first result 207 | getLoginData.bind(logins[0])(); 208 | } else { 209 | // don't search without input. 210 | if (!this.s.value.length) { 211 | return; 212 | } 213 | 214 | // search for matching entries 215 | searchPassword(this.s.value, "search", false); 216 | } 217 | } 218 | 219 | function init(tab) { 220 | // do nothing if called from a non-tab context 221 | if (!tab || !tab.url) { 222 | return; 223 | } 224 | 225 | activeTab = tab; 226 | var activeDomain = parseDomainFromUrl(tab.url); 227 | searchPassword(activeDomain, "match_domain"); 228 | } 229 | 230 | function searchPassword(_domain, action = "search", useFillOnSubmit = true) { 231 | // don't run searches for empty queries or ignored URLs 232 | _domain = _domain.trim(); 233 | var ignore = ["newtab", "extensions"]; 234 | if (!_domain.length || ignore.indexOf(_domain) >= 0) { 235 | return; 236 | } 237 | 238 | searching = true; 239 | logins = resultLogins = []; 240 | domain = _domain; 241 | urlDuringSearch = activeTab.url; 242 | m.redraw(); 243 | 244 | // First get the settings needed by the browserpass native client 245 | // by requesting them from the background script (which has localStorage access 246 | // to the settings). Then construct the message to send to browserpass and 247 | // send that via sendNativeMessage. 248 | chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) { 249 | searchSettings = settings; 250 | chrome.runtime.sendNativeMessage( 251 | app, 252 | { action: action, domain: _domain, settings: settings }, 253 | function(response) { 254 | if (chrome.runtime.lastError) { 255 | return resetWithError(chrome.runtime.lastError.message); 256 | } 257 | 258 | if (typeof response == "string") { 259 | return resetWithError(response); 260 | } 261 | 262 | searching = false; 263 | 264 | logins = resultLogins = response ? response : []; 265 | document.getElementById("filter-search").textContent = domain; 266 | fillOnSubmit = useFillOnSubmit && logins.length > 0; 267 | if (logins.length > 0) { 268 | showFilterHint(true); 269 | document.getElementById("search-field").value = ""; 270 | } 271 | m.redraw(); 272 | } 273 | ); 274 | }); 275 | } 276 | 277 | function parseDomainFromUrl(url) { 278 | var a = document.createElement("a"); 279 | a.href = url; 280 | return a.hostname; 281 | } 282 | 283 | function getFaviconUrl(domain) { 284 | // use current favicon when searching for current tab 285 | if ( 286 | activeTab && 287 | activeTab.favIconUrl && 288 | activeTab.favIconUrl.indexOf(domain) > -1 289 | ) { 290 | return activeTab.favIconUrl; 291 | } 292 | 293 | return null; 294 | } 295 | 296 | function launchURL(event) { 297 | var openInNewTab = event.shiftKey; 298 | 299 | chrome.runtime.sendMessage( 300 | { action: "launch", entry: this.entry, openInNewTab: openInNewTab }, 301 | function(response) { 302 | if (response.error) { 303 | return resetWithError(response.error); 304 | } 305 | window.close(); 306 | } 307 | ); 308 | } 309 | 310 | function getLoginData() { 311 | searching = true; 312 | logins = resultLogins = []; 313 | m.redraw(); 314 | 315 | chrome.runtime.sendMessage( 316 | { action: "login", entry: this, urlDuringSearch: urlDuringSearch }, 317 | function(response) { 318 | if (response.error) { 319 | return resetWithError(response.error); 320 | } 321 | window.close(); 322 | } 323 | ); 324 | } 325 | 326 | function loginToClipboard() { 327 | chrome.runtime.sendMessage( 328 | { action: "copyToClipboard", entry: this.entry, what: this.what }, 329 | function(response) { 330 | if (response.error) { 331 | return resetWithError(response.error); 332 | } 333 | 334 | copyToClipboard(response.text); 335 | window.close(); 336 | } 337 | ); 338 | } 339 | 340 | function copyToClipboard(s) { 341 | var clipboardContainer = document.getElementById("clipboard-container"); 342 | var clipboard = document.createElement("input"); 343 | clipboardContainer.appendChild(clipboard); 344 | clipboard.value = s; 345 | clipboard.select(); 346 | document.execCommand("copy"); 347 | clipboard.blur(); 348 | clipboardContainer.removeChild(clipboard); 349 | } 350 | 351 | // This function uses regular DOM 352 | // therefore there is no need for redraw calls 353 | function keyHandler(e) { 354 | switch (e.key) { 355 | case "ArrowUp": 356 | switchFocus("div.entry:last-child > .login", "previousElementSibling"); 357 | break; 358 | 359 | case "ArrowDown": 360 | switchFocus("div.entry:first-child > .login", "nextElementSibling"); 361 | break; 362 | case "ArrowRight": 363 | if (document.activeElement.nextElementSibling) { 364 | document.activeElement.nextElementSibling.focus(); 365 | } 366 | break; 367 | case "ArrowLeft": 368 | if (document.activeElement.previousElementSibling) { 369 | document.activeElement.previousElementSibling.focus(); 370 | } 371 | break; 372 | case "c": 373 | if (e.target.id != "search-field" && e.ctrlKey) { 374 | document.activeElement.parentNode 375 | .querySelector("button.copy.password") 376 | .click(); 377 | } 378 | break; 379 | case "C": 380 | if (e.target.id != "search-field") { 381 | document.activeElement.parentNode 382 | .querySelector("button.copy.username") 383 | .click(); 384 | } 385 | break; 386 | case "g": 387 | case "G": 388 | if (e.target.id != "search-field") { 389 | var mouseClickEvent = new MouseEvent("click", { 390 | bubbles: true, 391 | cancelable: true, 392 | view: e.view, 393 | shiftKey: e.shiftKey 394 | }); 395 | document.activeElement.parentNode 396 | .querySelector("button.launch.url") 397 | .dispatchEvent(mouseClickEvent); 398 | } 399 | } 400 | } 401 | 402 | function switchFocus(firstSelector, nextNodeAttr) { 403 | var searchField = document.getElementById("search-field"); 404 | var newActive = searchField; 405 | 406 | if (document.activeElement === searchField) { 407 | newActive = document.querySelector(firstSelector); 408 | } else { 409 | let tmp = document.activeElement["parentElement"][nextNodeAttr]; 410 | if (tmp !== null) { 411 | newActive = tmp["firstElementChild"]; 412 | } 413 | } 414 | 415 | newActive.focus(); 416 | } 417 | 418 | // The oncreate(vnode) hook is called after a DOM element is created and attached to the document. 419 | // see https://mithril.js.org/lifecycle-methods.html#oncreate for mor informations 420 | function oncreate() { 421 | // FireFox probably prevents `focus()` calls for some time 422 | // after extension is rendered. 423 | window.setTimeout(function() { 424 | document.getElementById("search-field").focus(); 425 | }, 100); 426 | } 427 | 428 | function resetWithError(errMsg) { 429 | console.error(errMsg); 430 | domain = ""; 431 | logins = resultLogins = []; 432 | fillOnSubmit = false; 433 | searching = false; 434 | var filterSearch = document.getElementById("filter-search"); 435 | filterSearch.style.display = "none"; 436 | filterSearch.textContent = ""; 437 | var searchField = document.getElementById("search-field"); 438 | searchField.setAttribute("placeholder", "Search passwords..."); 439 | error = errMsg; 440 | m.redraw(); 441 | searchField.focus(); 442 | } 443 | -------------------------------------------------------------------------------- /chrome/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | font-family: sans-serif; 5 | min-width: 350px; 6 | background: white; 7 | font-size: 14px; 8 | overflow-y: hidden; 9 | } 10 | 11 | .search > form { 12 | border-bottom: 1px solid #bbb; 13 | display: flex; 14 | flex-wrap: nowrap; 15 | } 16 | 17 | .search > form :last-child { 18 | width: 100%; 19 | } 20 | 21 | .search input { 22 | box-sizing: border-box; 23 | width: 100%; 24 | padding: 6px; 25 | border: 0; 26 | background: url("icon-search.svg") center right 6px no-repeat; 27 | background-size: 16px 16px; 28 | background-color: white; 29 | color: black; 30 | padding-right: 20px; 31 | } 32 | 33 | #filter-search { 34 | background: #eee; 35 | border: 0; 36 | box-sizing: border-box; 37 | display: none; 38 | padding: 6px; 39 | padding-top: 5px; 40 | white-space: nowrap; 41 | } 42 | 43 | .search input:focus { 44 | outline: 0; 45 | } 46 | 47 | .status-text { 48 | padding: 6px; 49 | } 50 | 51 | .results { 52 | width: 100%; 53 | min-width: 160px; 54 | } 55 | 56 | .entry { 57 | display: flex; 58 | flex-flow: row; 59 | border: 0; 60 | border-bottom: 1px dotted #ccc; 61 | } 62 | 63 | .copy, 64 | .launch { 65 | width: 32px; 66 | border: 0; 67 | cursor: pointer; 68 | } 69 | 70 | .url { 71 | background: no-repeat url("icon-globe.svg") center; 72 | background-size: 16px 16px; 73 | } 74 | 75 | .username { 76 | background: no-repeat url("icon-user.svg") center; 77 | background-size: 16px 16px; 78 | } 79 | 80 | .password { 81 | background: no-repeat url("icon-key.svg") center; 82 | background-size: 16px 16px; 83 | } 84 | 85 | .otp { 86 | background: no-repeat url("icon-otp.svg") center; 87 | background-size: 16px 16px; 88 | } 89 | 90 | .login { 91 | display: flex; 92 | flex: 1; 93 | -webkit-appearance: none; 94 | -moz-appearance: none; 95 | padding: 0 0 0 12px; 96 | cursor: pointer; 97 | border: 0; 98 | text-align: left; 99 | background-color: white; 100 | color: black; 101 | line-height: 1.2; 102 | white-space: nowrap; 103 | } 104 | 105 | .login.favicon { 106 | background-repeat: no-repeat; 107 | background-position: left 8px center; 108 | background-size: 16px 16px; 109 | padding-left: 32px; 110 | } 111 | 112 | .login .store { 113 | background-color: #090; 114 | border-radius: 4px; 115 | color: #fff; 116 | height: calc(100% - 22px); 117 | line-height: 100%; 118 | margin: 10px 4px 10px 0; 119 | padding: 2px 4px; 120 | } 121 | 122 | .login .name { 123 | height: 100%; 124 | line-height: 100%; 125 | padding: 12px 0; 126 | } 127 | 128 | .entry:last-of-type { 129 | border-bottom: 0; 130 | } 131 | 132 | .entry > *:hover, 133 | .entry > *:focus { 134 | outline: 0; 135 | background-color: #eee; 136 | } 137 | 138 | .loader { 139 | margin: 12px auto; 140 | display: block; 141 | text-indent: -9999999px; 142 | border: 3px solid rgba(0, 0, 0, 0.2); 143 | border-left-color: #000000; 144 | -webkit-transform: translateZ(0); 145 | -webkit-animation: load8 1.1s infinite linear; 146 | overflow: hidden; 147 | border-radius: 50%; 148 | vertical-align: middle; 149 | width: 12px; 150 | height: 12px; 151 | } 152 | 153 | #clipboard-container { 154 | position: fixed; 155 | top: 0px; 156 | left: 0px; 157 | width: 0px; 158 | height: 0px; 159 | z-index: 100; 160 | opacity: 0; 161 | } 162 | 163 | #clipboard { 164 | width: 1px; 165 | height: 1px; 166 | } 167 | 168 | @keyframes load8 { 169 | 0% { 170 | -webkit-transform: rotate(0deg); 171 | } 172 | 100% { 173 | -webkit-transform: rotate(360deg); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /cmd/browserpass/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/dannyvankooten/browserpass" 11 | "github.com/dannyvankooten/browserpass/protector" 12 | ) 13 | 14 | const VERSION = "2.0.22" 15 | 16 | func main() { 17 | protector.Protect("stdio rpath proc exec getpw") 18 | log.SetPrefix("[Browserpass] ") 19 | 20 | showVersion := flag.Bool("v", false, "print version and exit") 21 | flag.Parse() 22 | if *showVersion { 23 | fmt.Println("Browserpass host app version:", VERSION) 24 | os.Exit(0) 25 | } 26 | 27 | if err := browserpass.Run(os.Stdin, os.Stdout); err != nil && err != io.EOF { 28 | log.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /firefox/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.dannyvankooten.browserpass", 3 | "description": "Browserpass binary for the Firefox extension", 4 | "path": "%%replace%%", 5 | "type": "stdio", 6 | "allowed_extensions": [ 7 | "browserpass@maximbaz.com" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "browserpass-ce", 4 | "description": "Firefox extension for zx2c4's pass (password manager) - Community Edition.", 5 | "version": "2.0.23", 6 | "author": "Danny van Kooten", 7 | "homepage_url": "https://github.com/dannyvankooten/browserpass", 8 | "options_ui": { 9 | "page": "options.html" 10 | }, 11 | "background": { 12 | "persistent": true, 13 | "scripts": [ 14 | "background.js" 15 | ] 16 | }, 17 | "browser_action": { 18 | "browser_style": false, 19 | "default_icon": "icon-lock.png", 20 | "default_popup": "content.html" 21 | }, 22 | "permissions": [ 23 | "clipboardWrite", 24 | "tabs", 25 | "activeTab", 26 | "nativeMessaging", 27 | "notifications", 28 | "storage", 29 | "webRequest", 30 | "webRequestBlocking", 31 | "http://*/*", 32 | "https://*/*" 33 | ], 34 | "commands": { 35 | "_execute_browser_action": { 36 | "suggested_key": { 37 | "default": "Ctrl+Shift+L" 38 | } 39 | } 40 | }, 41 | "applications": { 42 | "gecko": { 43 | "id": "browserpass@maximbaz.com", 44 | "strict_min_version": "50.0" 45 | } 46 | }, 47 | "web_accessible_resources": [ 48 | "otp.html" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | # Installs the native manifest on windows 2 | # 3 | 4 | $app = 'com.dannyvankooten.browserpass' 5 | 6 | $dirpath = Join-Path -Path $env:localappdata -ChildPath 'browserpass' 7 | $ff_jsonpath = Join-Path -Path $dirpath -ChildPath "$app-firefox.json" 8 | $chrome_jsonpath = Join-Path -Path $dirpath -ChildPath "$app-chrome.json" 9 | 10 | # Make our local directory 11 | new-item -type Directory -Path $dirpath -force 12 | 13 | # copy our bin to local directory 14 | & Copy-Item browserpass-windows64.exe $dirpath 15 | 16 | # copy the native messaging manifest 17 | $ffile = gc firefox-host.json 18 | $ffile -replace '%%replace%%', ((Join-Path -Path $dirpath -ChildPath 'browserpass-windows64.exe' | ConvertTo-json) -replace '^"|"$', "") | Out-File -Encoding UTF8 $ff_jsonpath 19 | 20 | $cfile = gc chrome-host.json 21 | $cfile -replace '%%replace%%', ((Join-Path -Path $dirpath -ChildPath 'browserpass-windows64.exe' | ConvertTo-json) -replace '^"|"$', "") | Out-File -Encoding UTF8 $chrome_jsonpath 22 | 23 | Write-Host "" 24 | Write-Host "Which browser are you using?" 25 | Write-Host "1) Firefox" 26 | Write-Host "2) Chrome" 27 | 28 | $browser = Read-Host 29 | $allUsers = Read-Host "Install for all users? (y/n) [n]" 30 | 31 | $installDest = "cu" # Current User 32 | switch -wildcard ($allUsers) { 33 | "y*" { $installDest = "lm"; Break; } 34 | "n*" { $installDest = "cu"; Break; } 35 | default { $installDest = "cu" } 36 | } 37 | 38 | $browserToUse = "" 39 | switch -regex ($browser) { 40 | '^1$|^f' { $browserToUse = "Mozilla"; Break; } 41 | '^2$|^c' { $browserToUse = "Google/Chrome"; Break; } 42 | default {$browserToUse = "unknown"} 43 | } 44 | 45 | If ($installDest -eq "lm" -And -NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(` 46 | [Security.Principal.WindowsBuiltInRole] "Administrator")) { 47 | Write-Warning "Please re-run this script with Admin rights!" 48 | exit 49 | } 50 | 51 | $regPath = "hk{0}:\Software\{1}\NativeMessagingHosts" -f $installDest, $browserToUse 52 | 53 | If (-NOT (Test-Path -Path $regPath)) { 54 | New-Item -Path $regPath -force 55 | } 56 | New-Item -Path "$regPath\$app" -force 57 | New-ItemProperty -Path "$regPath\$app"` 58 | -Name '(Default)'` 59 | -Value $(If ($browserToUse -eq "Mozilla") {$ff_jsonpath} Else {$chrome_jsonpath}) -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | assert_file_exists() { 6 | if [ ! -f "$1" ]; then 7 | echo "ERROR: '$1' is missing." 8 | echo "If you are running './install.sh' from a release archive, please file a bug." 9 | echo "If you are running './install.sh' from the source code, make sure to follow CONTRIBUTING.md on how to build first." 10 | exit 1 11 | fi 12 | } 13 | 14 | BIN_DIR="$( cd "$( dirname "$0" )" && pwd )" 15 | JSON_DIR="$BIN_DIR" 16 | APP_NAME="com.dannyvankooten.browserpass" 17 | HOST_FILE="$BIN_DIR/browserpass" 18 | BROWSER="$1" 19 | 20 | # Find target dirs for various browsers & OS'es 21 | # https://developer.chrome.com/extensions/nativeMessaging#native-messaging-host-location 22 | # https://wiki.mozilla.org/WebExtensions/Native_Messaging 23 | OPERATING_SYSTEM=$(uname -s) 24 | 25 | case $OPERATING_SYSTEM in 26 | Linux) 27 | HOST_FILE="$BIN_DIR/browserpass-linux64" 28 | if [ "$(whoami)" == "root" ]; then 29 | TARGET_DIR_CHROME="/etc/opt/chrome/native-messaging-hosts" 30 | TARGET_DIR_CHROMIUM="/etc/chromium/native-messaging-hosts" 31 | TARGET_DIR_FIREFOX="/usr/lib/mozilla/native-messaging-hosts" 32 | TARGET_DIR_VIVALDI="/etc/chromium/native-messaging-hosts" 33 | else 34 | TARGET_DIR_CHROME="$HOME/.config/google-chrome/NativeMessagingHosts" 35 | TARGET_DIR_CHROMIUM="$HOME/.config/chromium/NativeMessagingHosts" 36 | TARGET_DIR_FIREFOX="$HOME/.mozilla/native-messaging-hosts" 37 | TARGET_DIR_VIVALDI="$HOME/.config/vivaldi/NativeMessagingHosts" 38 | fi 39 | ;; 40 | Darwin) 41 | HOST_FILE="$BIN_DIR/browserpass-darwinx64" 42 | if [ "$(whoami)" == "root" ]; then 43 | TARGET_DIR_CHROME="/Library/Google/Chrome/NativeMessagingHosts" 44 | TARGET_DIR_CHROMIUM="/Library/Application Support/Chromium/NativeMessagingHosts" 45 | TARGET_DIR_FIREFOX="/Library/Application Support/Mozilla/NativeMessagingHosts" 46 | TARGET_DIR_VIVALDI="/Library/Application Support/Vivaldi/NativeMessagingHosts" 47 | else 48 | TARGET_DIR_CHROME="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts" 49 | TARGET_DIR_CHROMIUM="$HOME/Library/Application Support/Chromium/NativeMessagingHosts" 50 | TARGET_DIR_FIREFOX="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts" 51 | TARGET_DIR_VIVALDI="$HOME/Library/Application Support/Vivaldi/NativeMessagingHosts" 52 | fi 53 | ;; 54 | OpenBSD) 55 | HOST_FILE="$BIN_DIR/browserpass-openbsd64" 56 | if [ "$(whoami)" == "root" ]; then 57 | echo "Installing as root not supported." 58 | exit 1 59 | fi 60 | TARGET_DIR_CHROME="$HOME/.config/google-chrome/NativeMessagingHosts" 61 | TARGET_DIR_CHROMIUM="$HOME/.config/chromium/NativeMessagingHosts" 62 | TARGET_DIR_FIREFOX="$HOME/.mozilla/native-messaging-hosts" 63 | TARGET_DIR_VIVALDI="$HOME/.config/vivaldi/NativeMessagingHosts" 64 | ;; 65 | FreeBSD) 66 | HOST_FILE="$BIN_DIR/browserpass-freebsd64" 67 | if [ "$(whoami)" == "root" ]; then 68 | echo "Installing as root not supported" 69 | exit 1 70 | fi 71 | TARGET_DIR_CHROME="$HOME/.config/google-chrome/NativeMessagingHosts" 72 | TARGET_DIR_CHROMIUM="$HOME/.config/chromium/NativeMessagingHosts" 73 | TARGET_DIR_FIREFOX="$HOME/.mozilla/native-messaging-hosts" 74 | TARGET_DIR_VIVALDI="$HOME/.config/vivaldi/NativeMessagingHosts" 75 | ;; 76 | *) 77 | echo "$OPERATING_SYSTEM is not supported" 78 | exit 1 79 | ;; 80 | esac 81 | 82 | if [ -e "$BIN_DIR/browserpass" ]; then 83 | echo "Detected development binary" 84 | HOST_FILE="$BIN_DIR/browserpass" 85 | fi 86 | 87 | if [ -z "$BROWSER" ]; then 88 | echo "" 89 | echo "Select your browser:" 90 | echo "====================" 91 | echo "1) Chrome" 92 | echo "2) Chromium" 93 | echo "3) Firefox" 94 | echo "4) Vivaldi" 95 | echo -n "1-4: " 96 | read BROWSER 97 | echo "" 98 | fi 99 | 100 | # Set target dir from user input 101 | case $BROWSER in 102 | 1|[Cc]hrome) 103 | BROWSER_NAME="Chrome" 104 | TARGET_DIR="$TARGET_DIR_CHROME" 105 | ;; 106 | 2|[Cc]hromium) 107 | BROWSER_NAME="Chromium" 108 | TARGET_DIR="$TARGET_DIR_CHROMIUM" 109 | ;; 110 | 3|[Ff]irefox) 111 | BROWSER_NAME="Firefox" 112 | TARGET_DIR="$TARGET_DIR_FIREFOX" 113 | ;; 114 | 4|[Vv]ivaldi) 115 | BROWSER_NAME="Vivaldi" 116 | TARGET_DIR="$TARGET_DIR_VIVALDI" 117 | ;; 118 | *) 119 | echo "Invalid selection. Please select 1-4 or one of the browser names." 120 | exit 1 121 | ;; 122 | esac 123 | 124 | echo "Installing $BROWSER_NAME host config" 125 | 126 | # Create config dir if not existing 127 | mkdir -p "$TARGET_DIR" 128 | 129 | if [ "$BROWSER_NAME" == "Firefox" ]; then 130 | MANIFEST="$JSON_DIR/firefox-host.json" 131 | else 132 | MANIFEST="$JSON_DIR/chrome-host.json" 133 | POLICY="$JSON_DIR/chrome-policy.json" 134 | fi 135 | 136 | # Copy native host manifest, filling in binary path 137 | assert_file_exists "$MANIFEST" 138 | sed "s/%%replace%%/${HOST_FILE////\\/}/" "$MANIFEST" \ 139 | > "$TARGET_DIR/$APP_NAME.json" 140 | 141 | # Set permissions for the manifest so that all users can read it. 142 | chmod o+r "$TARGET_DIR/$APP_NAME.json" 143 | 144 | # Copy policy file, if any 145 | if [ -n "$POLICY" ]; then 146 | assert_file_exists "$POLICY" 147 | POLICY_DIR="$TARGET_DIR"/../policies/managed/ 148 | mkdir -p "$POLICY_DIR" 149 | cp "$POLICY" "$POLICY_DIR/$APP_NAME.json" 150 | fi 151 | 152 | echo "Native messaging host for $BROWSER_NAME has been installed to $TARGET_DIR." 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "prettier": "prettier" 4 | }, 5 | "dependencies": { 6 | "browserify": "^14.4.0", 7 | "fuzzysort": "^1.1.0", 8 | "mithril": "^1.1.4", 9 | "prettier": "^1.11.1", 10 | "tldjs": "^2.3.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pass/disk.go: -------------------------------------------------------------------------------- 1 | package pass 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | 11 | "os/user" 12 | 13 | "github.com/mattn/go-zglob" 14 | sfuzzy "github.com/sahilm/fuzzy" 15 | ) 16 | 17 | // StoreDefinition defines a password store object 18 | type StoreDefinition struct { 19 | Name string `json:"name"` 20 | Path string `json:"path"` 21 | } 22 | 23 | type diskStore struct { 24 | stores []StoreDefinition 25 | useFuzzy bool // Setting for FuzzySearch or GlobSearch in manual searches 26 | } 27 | 28 | func NewDefaultStore(stores []StoreDefinition, useFuzzy bool) (Store, error) { 29 | if stores == nil || len(stores) == 0 { 30 | defaultPath, err := defaultStorePath() 31 | if err != nil { 32 | return nil, err 33 | } 34 | stores = []StoreDefinition{{Name: "default", Path: defaultPath}} 35 | } 36 | 37 | // Expand paths, follow symlinks 38 | for i, store := range stores { 39 | path := store.Path 40 | if strings.HasPrefix(path, "~/") { 41 | path = filepath.Join("$HOME", path[2:]) 42 | } 43 | path = os.ExpandEnv(path) 44 | path, err := filepath.EvalSymlinks(path) 45 | if err != nil { 46 | return nil, err 47 | } 48 | stores[i].Path = path 49 | } 50 | 51 | return &diskStore{stores, useFuzzy}, nil 52 | } 53 | 54 | func defaultStorePath() (string, error) { 55 | path := os.Getenv("PASSWORD_STORE_DIR") 56 | if path != "" { 57 | return path, nil 58 | } 59 | 60 | usr, err := user.Current() 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | path = filepath.Join(usr.HomeDir, ".password-store") 66 | return path, nil 67 | } 68 | 69 | // Do a search. Will call into the correct algoritm (glob or fuzzy) 70 | // based on the settings present in the diskStore struct 71 | func (s *diskStore) Search(query string) ([]string, error) { 72 | if s.useFuzzy { 73 | return s.FuzzySearch(query) 74 | } 75 | return s.GlobSearch(query) 76 | } 77 | 78 | // Fuzzy searches first get a list of all pass entries by doing a glob search 79 | // for the empty string, then apply appropriate logic to convert results to 80 | // a slice of strings, finally returning all of the unique entries. 81 | func (s *diskStore) FuzzySearch(query string) ([]string, error) { 82 | entries, err := s.GlobSearch("") 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | // GlobSearch now results `storename:filename`, for fuzzy search we need to provide only file names 88 | var fileNames []string 89 | for _, entry := range entries { 90 | fileNames = append(fileNames, strings.SplitN(entry, ":", 2)[1]) 91 | } 92 | 93 | // The resulting match struct does not copy the strings, but rather 94 | // provides the index to the original array. Copy those strings 95 | // into the result slice 96 | var results []string 97 | matches := sfuzzy.Find(query, fileNames) 98 | for _, match := range matches { 99 | results = append(results, entries[match.Index]) 100 | } 101 | 102 | return results, nil 103 | } 104 | 105 | func (s *diskStore) GlobSearch(query string) ([]string, error) { 106 | // Search: 107 | // 1. DOMAIN/USERNAME.gpg 108 | // 2. DOMAIN.gpg 109 | // 3. DOMAIN/SUBDIRECTORY/USERNAME.gpg 110 | 111 | items := []string{} 112 | 113 | for _, store := range s.stores { 114 | matches, err := zglob.GlobFollowSymlinks(store.Path + "/**/" + query + "*/**/*.gpg") 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | matches2, err := zglob.GlobFollowSymlinks(store.Path + "/**/" + query + "*.gpg") 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | allMatches := append(matches, matches2...) 125 | 126 | for i, match := range allMatches { 127 | item, err := filepath.Rel(store.Path, match) 128 | if err != nil { 129 | return nil, err 130 | } 131 | allMatches[i] = store.Name + ":" + strings.TrimSuffix(item, ".gpg") 132 | } 133 | 134 | items = append(items, allMatches...) 135 | } 136 | 137 | if strings.Count(query, ".") >= 2 { 138 | // try finding additional items by removing subparts of the query 139 | queryParts := strings.SplitN(query, ".", 2)[1:] 140 | newItems, err := s.GlobSearch(strings.Join(queryParts, ".")) 141 | if err != nil { 142 | return nil, err 143 | } 144 | items = append(items, newItems...) 145 | } 146 | 147 | result := unique(items) 148 | sort.Strings(result) 149 | 150 | return result, nil 151 | } 152 | 153 | func (s *diskStore) Open(item string) (io.ReadCloser, error) { 154 | parts := strings.SplitN(item, ":", 2) 155 | 156 | for _, store := range s.stores { 157 | if store.Name != parts[0] { 158 | continue 159 | } 160 | path := filepath.Join(store.Path, parts[1]+".gpg") 161 | f, err := os.Open(path) 162 | if os.IsNotExist(err) { 163 | continue 164 | } 165 | return f, err 166 | } 167 | return nil, errors.New("Unable to find the item on disk") 168 | } 169 | 170 | func unique(elems []string) []string { 171 | seen := make(map[string]bool) 172 | result := []string{} 173 | for _, elem := range elems { 174 | if !seen[elem] { 175 | seen[elem] = true 176 | result = append(result, elem) 177 | } 178 | } 179 | return result 180 | } 181 | -------------------------------------------------------------------------------- /pass/disk_test.go: -------------------------------------------------------------------------------- 1 | package pass 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestDefaultStorePath(t *testing.T) { 11 | var expectedCustom, expected, actual string 12 | 13 | usr, err := user.Current() 14 | 15 | if err != nil { 16 | t.Log("Unable to retrieve current user, skipping part of the test. Error: ", err) 17 | } else { 18 | // default directory 19 | os.Setenv("PASSWORD_STORE_DIR", "") 20 | expected = filepath.Join(usr.HomeDir, ".password-store") 21 | actual, _ = defaultStorePath() 22 | 23 | if expected != actual { 24 | t.Errorf("1: '%s' does not match '%s'", expected, actual) 25 | } 26 | } 27 | 28 | // custom directory from $PASSWORD_STORE_DIR 29 | expected, err = filepath.Abs("browserpass-test") 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | 34 | os.Mkdir(expectedCustom, os.ModePerm) 35 | os.Setenv("PASSWORD_STORE_DIR", expected) 36 | actual, err = defaultStorePath() 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | if expected != actual { 41 | t.Errorf("2: '%s' does not match '%s'", expected, actual) 42 | } 43 | 44 | // clean-up 45 | os.Setenv("PASSWORD_STORE_DIR", "") 46 | os.Remove(expected) 47 | } 48 | 49 | func TestDiskStore_Search_nomatch(t *testing.T) { 50 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} 51 | 52 | domain := "this-most-definitely-does-not-exist" 53 | logins, err := store.Search(domain) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | if len(logins) > 0 { 58 | t.Errorf("%s yielded results, but it should not", domain) 59 | } 60 | } 61 | 62 | func TestDiskStoreSearch(t *testing.T) { 63 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} 64 | expectedResult := "default:abc.com" 65 | testDomains := []string{"abc.com", "test.abc.com", "testing.test.abc.com"} 66 | for _, domain := range testDomains { 67 | searchResults, err := store.Search(domain) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | // check if result contains abc.com 72 | found := false 73 | for _, searchResult := range searchResults { 74 | if searchResult == expectedResult { 75 | found = true 76 | break 77 | } 78 | } 79 | if found != true { 80 | t.Fatalf("Couldn't find %v in %v", expectedResult, searchResults) 81 | } 82 | } 83 | } 84 | 85 | func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testing.T) { 86 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} 87 | searchResult, err := store.Search("xyz") 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | if len(searchResult) != 1 { 92 | t.Fatalf("Found %v results instead of 1", len(searchResult)) 93 | } 94 | expectedResult := "default:xyz.com/xyz_user" 95 | if searchResult[0] != expectedResult { 96 | t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult[0]) 97 | } 98 | } 99 | 100 | func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) { 101 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} 102 | searchResult, err := store.Search("def.com") 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | if len(searchResult) != 1 { 107 | t.Fatalf("Found %v results instead of 1", len(searchResult)) 108 | } 109 | expectedResult := "default:def.com" 110 | if searchResult[0] != expectedResult { 111 | t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult[0]) 112 | } 113 | } 114 | 115 | func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) { 116 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} 117 | searchResult, err := store.Search("amazon.co.uk") 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | if len(searchResult) != 2 { 122 | t.Fatalf("Found %v results instead of 2", len(searchResult)) 123 | } 124 | expectedResult := []string{"default:amazon.co.uk/user1", "default:amazon.co.uk/user2"} 125 | if searchResult[0] != expectedResult[0] || searchResult[1] != expectedResult[1] { 126 | t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult) 127 | } 128 | } 129 | 130 | func TestDiskStoreSearchSubDirectories(t *testing.T) { 131 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} 132 | searchTermsMatches := map[string][]string{ 133 | "abc.org": []string{"default:abc.org/user3", "default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"}, 134 | "wiki": []string{"default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"}, 135 | "work": []string{"default:abc.org/wiki/work/user5"}, 136 | } 137 | 138 | for term, expectedResult := range searchTermsMatches { 139 | searchResult, err := store.Search(term) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | if len(searchResult) != len(expectedResult) { 144 | t.Fatalf("For term %v found %v results (%v) instead of %v (%v)", term, len(searchResult), searchResult, len(expectedResult), expectedResult) 145 | } 146 | for i := 0; i < len(expectedResult); i++ { 147 | if searchResult[i] != expectedResult[i] { 148 | t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult) 149 | } 150 | } 151 | } 152 | } 153 | 154 | func TestDiskStorePartSearch(t *testing.T) { 155 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false} 156 | searchResult, err := store.Search("ab") 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | if len(searchResult) != 4 { 161 | t.Fatalf("Found %v results instead of 4", len(searchResult)) 162 | } 163 | expectedResult := []string{"default:abc.com", "default:abc.org/user3", "default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"} 164 | for i := 0; i < len(expectedResult); i++ { 165 | if searchResult[i] != expectedResult[i] { 166 | t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult) 167 | } 168 | } 169 | } 170 | 171 | func TestFuzzySearch(t *testing.T) { 172 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true} 173 | searchResult, err := store.Search("amaz2") 174 | 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | if len(searchResult) != 2 { 179 | t.Fatalf("Result size was: %d expected 2", len(searchResult)) 180 | } 181 | 182 | expectedResult := map[string]bool{ 183 | "default:amazon.co.uk/user2": true, 184 | "default:amazon.com/user2": true, 185 | } 186 | 187 | for _, res := range searchResult { 188 | if !expectedResult[res] { 189 | t.Fatalf("Result %s not expected!", res) 190 | } 191 | } 192 | } 193 | 194 | func TestFuzzySearchNoResult(t *testing.T) { 195 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true} 196 | searchResult, err := store.Search("vvv") 197 | 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | if len(searchResult) != 0 { 202 | t.Fatalf("Result size was: %d expected 0", len(searchResult)) 203 | } 204 | } 205 | 206 | func TestFuzzySearchTopLevelEntries(t *testing.T) { 207 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true} 208 | searchResult, err := store.Search("def") 209 | 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | if len(searchResult) != 1 { 214 | t.Fatalf("Result size was: %d expected 1", len(searchResult)) 215 | } 216 | 217 | expectedResult := map[string]bool{ 218 | "default:def.com": true, 219 | } 220 | 221 | for _, res := range searchResult { 222 | if !expectedResult[res] { 223 | t.Fatalf("Result %s not expected!", res) 224 | } 225 | } 226 | } 227 | 228 | func TestGlobSearchMultipleStores(t *testing.T) { 229 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}, StoreDefinition{Name: "custom", Path: "test_store_2"}}, useFuzzy: false} 230 | searchResults, err := store.Search("abc.com") 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | if len(searchResults) != 2 { 235 | t.Fatalf("Found %v results instead of 2", len(searchResults)) 236 | } 237 | expectedResults := []string{"custom:abc.com", "default:abc.com"} 238 | if searchResults[0] != expectedResults[0] || searchResults[1] != expectedResults[1] { 239 | t.Fatalf("Couldn't find %v, found %v instead", expectedResults, searchResults) 240 | } 241 | } 242 | 243 | func TestFuzzySearchMultipleStores(t *testing.T) { 244 | store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}, StoreDefinition{Name: "custom", Path: "test_store_2"}}, useFuzzy: true} 245 | searchResults, err := store.Search("abc.com") 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | if len(searchResults) != 2 { 250 | t.Fatalf("Found %v results instead of 2", len(searchResults)) 251 | } 252 | expectedResults := []string{"default:abc.com", "custom:abc.com"} 253 | if searchResults[0] != expectedResults[0] || searchResults[1] != expectedResults[1] { 254 | t.Fatalf("Couldn't find %v, found %v instead", expectedResults, searchResults) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /pass/pass.go: -------------------------------------------------------------------------------- 1 | // Package pass provides access to UNIX password stores. 2 | package pass 3 | 4 | import ( 5 | "io" 6 | ) 7 | 8 | // Store is a password store. 9 | type Store interface { 10 | Search(query string) ([]string, error) 11 | Open(item string) (io.ReadCloser, error) 12 | GlobSearch(query string) ([]string, error) 13 | } 14 | -------------------------------------------------------------------------------- /pass/test_store/abc.com.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/pass/test_store/abc.com.gpg -------------------------------------------------------------------------------- /pass/test_store/abc.org/user3.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/pass/test_store/abc.org/user3.gpg -------------------------------------------------------------------------------- /pass/test_store/abc.org/wiki/user4.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/pass/test_store/abc.org/wiki/user4.gpg -------------------------------------------------------------------------------- /pass/test_store/abc.org/wiki/work/user5.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/pass/test_store/abc.org/wiki/work/user5.gpg -------------------------------------------------------------------------------- /pass/test_store/amazon.co.uk: -------------------------------------------------------------------------------- 1 | amazon.com -------------------------------------------------------------------------------- /pass/test_store/amazon.com/user1.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/pass/test_store/amazon.com/user1.gpg -------------------------------------------------------------------------------- /pass/test_store/amazon.com/user2.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/pass/test_store/amazon.com/user2.gpg -------------------------------------------------------------------------------- /pass/test_store/def.com.gpg: -------------------------------------------------------------------------------- 1 | abc.com.gpg -------------------------------------------------------------------------------- /pass/test_store/xyz.com/xyz_user.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/pass/test_store/xyz.com/xyz_user.gpg -------------------------------------------------------------------------------- /pass/test_store_2/abc.com.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-legacy/3104951b6322de1cd96a6d3d37c05f3135377da2/pass/test_store_2/abc.com.gpg -------------------------------------------------------------------------------- /protector/protector_generic.go: -------------------------------------------------------------------------------- 1 | // +build !openbsd 2 | 3 | package protector 4 | 5 | func Protect(s string) { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /protector/protector_openbsd.go: -------------------------------------------------------------------------------- 1 | // +build openbsd 2 | 3 | package protector 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | func Protect(s string) { 8 | unix.Pledge(s, nil) 9 | } 10 | -------------------------------------------------------------------------------- /uninstall.ps1: -------------------------------------------------------------------------------- 1 | $app = 'com.dannyvankooten.browserpass' 2 | $dirpath = Join-Path -Path $env:localappdata -ChildPath 'browserpass' 3 | 4 | if (Test-Path -Path $dirPath) { 5 | Remove-Item -Recurse -Force $dirpath 6 | } 7 | 8 | If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(` 9 | [Security.Principal.WindowsBuiltInRole] "Administrator")) { 10 | Write-Warning "Please re-run this script with Admin rights!" 11 | exit 12 | } 13 | 14 | If (Test-Path -Path "hklm:\Software\Google\Chrome\NativeMessagingHosts\$app") { 15 | Write-Host "Uninstalling for Chrome - all users" 16 | Remove-Item -Path "hklm:\Software\Google\Chrome\NativeMessagingHosts\$app" -force 17 | } 18 | if (Test-Path -Path "hklm:\Software\Mozilla\NativeMessagingHosts\$app") { 19 | Write-Host "Uninstalling for Firefox - all users" 20 | Remove-Item -Path "hklm:\Software\Mozilla\NativeMessagingHosts\$app" -force 21 | } 22 | 23 | if (Test-Path -Path "hkcu:\Software\Mozilla\NativeMessagingHosts\$app") { 24 | Write-Host "Uninstalling for Firefox - local user" 25 | Remove-Item -Path "hkcu:\Software\Mozilla\NativeMessagingHosts\$app" -force 26 | } 27 | 28 | if (Test-Path -Path "hkcu:\Software\Google\Chrome\NativeMessagingHosts\$app") { 29 | Write-Host "Uninstalling for Chrome - local user" 30 | Remove-Item -Path "hkcu:\Software\Google\Chrome\NativeMessagingHosts\$app" -force 31 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | JSONStream@^1.0.3: 6 | version "1.3.2" 7 | resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea" 8 | dependencies: 9 | jsonparse "^1.2.0" 10 | through ">=2.2.7 <3" 11 | 12 | acorn-node@^1.2.0: 13 | version "1.3.0" 14 | resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.3.0.tgz#5f86d73346743810ef1269b901dbcbded020861b" 15 | dependencies: 16 | acorn "^5.4.1" 17 | xtend "^4.0.1" 18 | 19 | acorn@^4.0.3: 20 | version "4.0.13" 21 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" 22 | 23 | acorn@^5.2.1, acorn@^5.4.1: 24 | version "5.5.3" 25 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" 26 | 27 | array-filter@~0.0.0: 28 | version "0.0.1" 29 | resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" 30 | 31 | array-map@~0.0.0: 32 | version "0.0.0" 33 | resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" 34 | 35 | array-reduce@~0.0.0: 36 | version "0.0.0" 37 | resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" 38 | 39 | asn1.js@^4.0.0: 40 | version "4.10.1" 41 | resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" 42 | dependencies: 43 | bn.js "^4.0.0" 44 | inherits "^2.0.1" 45 | minimalistic-assert "^1.0.0" 46 | 47 | assert@^1.4.0: 48 | version "1.4.1" 49 | resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" 50 | dependencies: 51 | util "0.10.3" 52 | 53 | astw@^2.0.0: 54 | version "2.2.0" 55 | resolved "https://registry.yarnpkg.com/astw/-/astw-2.2.0.tgz#7bd41784d32493987aeb239b6b4e1c57a873b917" 56 | dependencies: 57 | acorn "^4.0.3" 58 | 59 | balanced-match@^1.0.0: 60 | version "1.0.0" 61 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 62 | 63 | base64-js@^1.0.2: 64 | version "1.3.0" 65 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" 66 | 67 | bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: 68 | version "4.11.8" 69 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" 70 | 71 | brace-expansion@^1.1.7: 72 | version "1.1.11" 73 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 74 | dependencies: 75 | balanced-match "^1.0.0" 76 | concat-map "0.0.1" 77 | 78 | brorand@^1.0.1: 79 | version "1.1.0" 80 | resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" 81 | 82 | browser-pack@^6.0.1: 83 | version "6.1.0" 84 | resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.1.0.tgz#c34ba10d0b9ce162b5af227c7131c92c2ecd5774" 85 | dependencies: 86 | JSONStream "^1.0.3" 87 | combine-source-map "~0.8.0" 88 | defined "^1.0.0" 89 | safe-buffer "^5.1.1" 90 | through2 "^2.0.0" 91 | umd "^3.0.0" 92 | 93 | browser-resolve@^1.11.0, browser-resolve@^1.7.0: 94 | version "1.11.2" 95 | resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" 96 | dependencies: 97 | resolve "1.1.7" 98 | 99 | browserify-aes@^1.0.0, browserify-aes@^1.0.4: 100 | version "1.2.0" 101 | resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" 102 | dependencies: 103 | buffer-xor "^1.0.3" 104 | cipher-base "^1.0.0" 105 | create-hash "^1.1.0" 106 | evp_bytestokey "^1.0.3" 107 | inherits "^2.0.1" 108 | safe-buffer "^5.0.1" 109 | 110 | browserify-cipher@^1.0.0: 111 | version "1.0.1" 112 | resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" 113 | dependencies: 114 | browserify-aes "^1.0.4" 115 | browserify-des "^1.0.0" 116 | evp_bytestokey "^1.0.0" 117 | 118 | browserify-des@^1.0.0: 119 | version "1.0.1" 120 | resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c" 121 | dependencies: 122 | cipher-base "^1.0.1" 123 | des.js "^1.0.0" 124 | inherits "^2.0.1" 125 | 126 | browserify-rsa@^4.0.0: 127 | version "4.0.1" 128 | resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" 129 | dependencies: 130 | bn.js "^4.1.0" 131 | randombytes "^2.0.1" 132 | 133 | browserify-sign@^4.0.0: 134 | version "4.0.4" 135 | resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" 136 | dependencies: 137 | bn.js "^4.1.1" 138 | browserify-rsa "^4.0.0" 139 | create-hash "^1.1.0" 140 | create-hmac "^1.1.2" 141 | elliptic "^6.0.0" 142 | inherits "^2.0.1" 143 | parse-asn1 "^5.0.0" 144 | 145 | browserify-zlib@~0.2.0: 146 | version "0.2.0" 147 | resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" 148 | dependencies: 149 | pako "~1.0.5" 150 | 151 | browserify@^14.4.0: 152 | version "14.5.0" 153 | resolved "https://registry.yarnpkg.com/browserify/-/browserify-14.5.0.tgz#0bbbce521acd6e4d1d54d8e9365008efb85a9cc5" 154 | dependencies: 155 | JSONStream "^1.0.3" 156 | assert "^1.4.0" 157 | browser-pack "^6.0.1" 158 | browser-resolve "^1.11.0" 159 | browserify-zlib "~0.2.0" 160 | buffer "^5.0.2" 161 | cached-path-relative "^1.0.0" 162 | concat-stream "~1.5.1" 163 | console-browserify "^1.1.0" 164 | constants-browserify "~1.0.0" 165 | crypto-browserify "^3.0.0" 166 | defined "^1.0.0" 167 | deps-sort "^2.0.0" 168 | domain-browser "~1.1.0" 169 | duplexer2 "~0.1.2" 170 | events "~1.1.0" 171 | glob "^7.1.0" 172 | has "^1.0.0" 173 | htmlescape "^1.1.0" 174 | https-browserify "^1.0.0" 175 | inherits "~2.0.1" 176 | insert-module-globals "^7.0.0" 177 | labeled-stream-splicer "^2.0.0" 178 | module-deps "^4.0.8" 179 | os-browserify "~0.3.0" 180 | parents "^1.0.1" 181 | path-browserify "~0.0.0" 182 | process "~0.11.0" 183 | punycode "^1.3.2" 184 | querystring-es3 "~0.2.0" 185 | read-only-stream "^2.0.0" 186 | readable-stream "^2.0.2" 187 | resolve "^1.1.4" 188 | shasum "^1.0.0" 189 | shell-quote "^1.6.1" 190 | stream-browserify "^2.0.0" 191 | stream-http "^2.0.0" 192 | string_decoder "~1.0.0" 193 | subarg "^1.0.0" 194 | syntax-error "^1.1.1" 195 | through2 "^2.0.0" 196 | timers-browserify "^1.0.1" 197 | tty-browserify "~0.0.0" 198 | url "~0.11.0" 199 | util "~0.10.1" 200 | vm-browserify "~0.0.1" 201 | xtend "^4.0.0" 202 | 203 | buffer-from@^1.0.0: 204 | version "1.0.0" 205 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" 206 | 207 | buffer-xor@^1.0.3: 208 | version "1.0.3" 209 | resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" 210 | 211 | buffer@^5.0.2: 212 | version "5.1.0" 213 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.1.0.tgz#c913e43678c7cb7c8bd16afbcddb6c5505e8f9fe" 214 | dependencies: 215 | base64-js "^1.0.2" 216 | ieee754 "^1.1.4" 217 | 218 | builtin-status-codes@^3.0.0: 219 | version "3.0.0" 220 | resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" 221 | 222 | cached-path-relative@^1.0.0: 223 | version "1.0.1" 224 | resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7" 225 | 226 | cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: 227 | version "1.0.4" 228 | resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" 229 | dependencies: 230 | inherits "^2.0.1" 231 | safe-buffer "^5.0.1" 232 | 233 | combine-source-map@^0.8.0, combine-source-map@~0.8.0: 234 | version "0.8.0" 235 | resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b" 236 | dependencies: 237 | convert-source-map "~1.1.0" 238 | inline-source-map "~0.6.0" 239 | lodash.memoize "~3.0.3" 240 | source-map "~0.5.3" 241 | 242 | concat-map@0.0.1: 243 | version "0.0.1" 244 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 245 | 246 | concat-stream@^1.6.1: 247 | version "1.6.2" 248 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" 249 | dependencies: 250 | buffer-from "^1.0.0" 251 | inherits "^2.0.3" 252 | readable-stream "^2.2.2" 253 | typedarray "^0.0.6" 254 | 255 | concat-stream@~1.5.0, concat-stream@~1.5.1: 256 | version "1.5.2" 257 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" 258 | dependencies: 259 | inherits "~2.0.1" 260 | readable-stream "~2.0.0" 261 | typedarray "~0.0.5" 262 | 263 | console-browserify@^1.1.0: 264 | version "1.1.0" 265 | resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" 266 | dependencies: 267 | date-now "^0.1.4" 268 | 269 | constants-browserify@~1.0.0: 270 | version "1.0.0" 271 | resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" 272 | 273 | convert-source-map@~1.1.0: 274 | version "1.1.3" 275 | resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" 276 | 277 | core-util-is@~1.0.0: 278 | version "1.0.2" 279 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 280 | 281 | create-ecdh@^4.0.0: 282 | version "4.0.1" 283 | resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.1.tgz#44223dfed533193ba5ba54e0df5709b89acf1f82" 284 | dependencies: 285 | bn.js "^4.1.0" 286 | elliptic "^6.0.0" 287 | 288 | create-hash@^1.1.0, create-hash@^1.1.2: 289 | version "1.2.0" 290 | resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" 291 | dependencies: 292 | cipher-base "^1.0.1" 293 | inherits "^2.0.1" 294 | md5.js "^1.3.4" 295 | ripemd160 "^2.0.1" 296 | sha.js "^2.4.0" 297 | 298 | create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: 299 | version "1.1.7" 300 | resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" 301 | dependencies: 302 | cipher-base "^1.0.3" 303 | create-hash "^1.1.0" 304 | inherits "^2.0.1" 305 | ripemd160 "^2.0.0" 306 | safe-buffer "^5.0.1" 307 | sha.js "^2.4.8" 308 | 309 | crypto-browserify@^3.0.0: 310 | version "3.12.0" 311 | resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" 312 | dependencies: 313 | browserify-cipher "^1.0.0" 314 | browserify-sign "^4.0.0" 315 | create-ecdh "^4.0.0" 316 | create-hash "^1.1.0" 317 | create-hmac "^1.1.0" 318 | diffie-hellman "^5.0.0" 319 | inherits "^2.0.1" 320 | pbkdf2 "^3.0.3" 321 | public-encrypt "^4.0.0" 322 | randombytes "^2.0.0" 323 | randomfill "^1.0.3" 324 | 325 | date-now@^0.1.4: 326 | version "0.1.4" 327 | resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" 328 | 329 | defined@^1.0.0: 330 | version "1.0.0" 331 | resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" 332 | 333 | deps-sort@^2.0.0: 334 | version "2.0.0" 335 | resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5" 336 | dependencies: 337 | JSONStream "^1.0.3" 338 | shasum "^1.0.0" 339 | subarg "^1.0.0" 340 | through2 "^2.0.0" 341 | 342 | des.js@^1.0.0: 343 | version "1.0.0" 344 | resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" 345 | dependencies: 346 | inherits "^2.0.1" 347 | minimalistic-assert "^1.0.0" 348 | 349 | detective@^4.0.0: 350 | version "4.7.1" 351 | resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" 352 | dependencies: 353 | acorn "^5.2.1" 354 | defined "^1.0.0" 355 | 356 | diffie-hellman@^5.0.0: 357 | version "5.0.3" 358 | resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" 359 | dependencies: 360 | bn.js "^4.1.0" 361 | miller-rabin "^4.0.0" 362 | randombytes "^2.0.0" 363 | 364 | domain-browser@~1.1.0: 365 | version "1.1.7" 366 | resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" 367 | 368 | duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: 369 | version "0.1.4" 370 | resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" 371 | dependencies: 372 | readable-stream "^2.0.2" 373 | 374 | elliptic@^6.0.0: 375 | version "6.4.0" 376 | resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" 377 | dependencies: 378 | bn.js "^4.4.0" 379 | brorand "^1.0.1" 380 | hash.js "^1.0.0" 381 | hmac-drbg "^1.0.0" 382 | inherits "^2.0.1" 383 | minimalistic-assert "^1.0.0" 384 | minimalistic-crypto-utils "^1.0.0" 385 | 386 | events@~1.1.0: 387 | version "1.1.1" 388 | resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" 389 | 390 | evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: 391 | version "1.0.3" 392 | resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" 393 | dependencies: 394 | md5.js "^1.3.4" 395 | safe-buffer "^5.1.1" 396 | 397 | fs.realpath@^1.0.0: 398 | version "1.0.0" 399 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 400 | 401 | function-bind@^1.0.2: 402 | version "1.1.1" 403 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 404 | 405 | fuzzysort@^1.1.0: 406 | version "1.1.1" 407 | resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.1.1.tgz#bf128f1a4cc6e6b7188665ac5676de46a3d81768" 408 | 409 | glob@^7.1.0: 410 | version "7.1.2" 411 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 412 | dependencies: 413 | fs.realpath "^1.0.0" 414 | inflight "^1.0.4" 415 | inherits "2" 416 | minimatch "^3.0.4" 417 | once "^1.3.0" 418 | path-is-absolute "^1.0.0" 419 | 420 | has@^1.0.0: 421 | version "1.0.1" 422 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" 423 | dependencies: 424 | function-bind "^1.0.2" 425 | 426 | hash-base@^3.0.0: 427 | version "3.0.4" 428 | resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" 429 | dependencies: 430 | inherits "^2.0.1" 431 | safe-buffer "^5.0.1" 432 | 433 | hash.js@^1.0.0, hash.js@^1.0.3: 434 | version "1.1.3" 435 | resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" 436 | dependencies: 437 | inherits "^2.0.3" 438 | minimalistic-assert "^1.0.0" 439 | 440 | hmac-drbg@^1.0.0: 441 | version "1.0.1" 442 | resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" 443 | dependencies: 444 | hash.js "^1.0.3" 445 | minimalistic-assert "^1.0.0" 446 | minimalistic-crypto-utils "^1.0.1" 447 | 448 | htmlescape@^1.1.0: 449 | version "1.1.1" 450 | resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" 451 | 452 | https-browserify@^1.0.0: 453 | version "1.0.0" 454 | resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" 455 | 456 | ieee754@^1.1.4: 457 | version "1.1.11" 458 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" 459 | 460 | indexof@0.0.1: 461 | version "0.0.1" 462 | resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" 463 | 464 | inflight@^1.0.4: 465 | version "1.0.6" 466 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 467 | dependencies: 468 | once "^1.3.0" 469 | wrappy "1" 470 | 471 | inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: 472 | version "2.0.3" 473 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 474 | 475 | inherits@2.0.1: 476 | version "2.0.1" 477 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" 478 | 479 | inline-source-map@~0.6.0: 480 | version "0.6.2" 481 | resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" 482 | dependencies: 483 | source-map "~0.5.3" 484 | 485 | insert-module-globals@^7.0.0: 486 | version "7.0.6" 487 | resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.6.tgz#15a31d9d394e76d08838b9173016911d7fd4ea1b" 488 | dependencies: 489 | JSONStream "^1.0.3" 490 | combine-source-map "^0.8.0" 491 | concat-stream "^1.6.1" 492 | is-buffer "^1.1.0" 493 | lexical-scope "^1.2.0" 494 | path-is-absolute "^1.0.1" 495 | process "~0.11.0" 496 | through2 "^2.0.0" 497 | xtend "^4.0.0" 498 | 499 | is-buffer@^1.1.0: 500 | version "1.1.6" 501 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 502 | 503 | isarray@^2.0.4: 504 | version "2.0.4" 505 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.4.tgz#38e7bcbb0f3ba1b7933c86ba1894ddfc3781bbb7" 506 | 507 | isarray@~1.0.0: 508 | version "1.0.0" 509 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 510 | 511 | json-stable-stringify@~0.0.0: 512 | version "0.0.1" 513 | resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" 514 | dependencies: 515 | jsonify "~0.0.0" 516 | 517 | jsonify@~0.0.0: 518 | version "0.0.0" 519 | resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 520 | 521 | jsonparse@^1.2.0: 522 | version "1.3.1" 523 | resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" 524 | 525 | labeled-stream-splicer@^2.0.0: 526 | version "2.0.1" 527 | resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.1.tgz#9cffa32fd99e1612fd1d86a8db962416d5292926" 528 | dependencies: 529 | inherits "^2.0.1" 530 | isarray "^2.0.4" 531 | stream-splicer "^2.0.0" 532 | 533 | lexical-scope@^1.2.0: 534 | version "1.2.0" 535 | resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4" 536 | dependencies: 537 | astw "^2.0.0" 538 | 539 | lodash.memoize@~3.0.3: 540 | version "3.0.4" 541 | resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" 542 | 543 | md5.js@^1.3.4: 544 | version "1.3.4" 545 | resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" 546 | dependencies: 547 | hash-base "^3.0.0" 548 | inherits "^2.0.1" 549 | 550 | miller-rabin@^4.0.0: 551 | version "4.0.1" 552 | resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" 553 | dependencies: 554 | bn.js "^4.0.0" 555 | brorand "^1.0.1" 556 | 557 | minimalistic-assert@^1.0.0: 558 | version "1.0.1" 559 | resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" 560 | 561 | minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: 562 | version "1.0.1" 563 | resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" 564 | 565 | minimatch@^3.0.4: 566 | version "3.0.4" 567 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 568 | dependencies: 569 | brace-expansion "^1.1.7" 570 | 571 | minimist@^1.1.0: 572 | version "1.2.0" 573 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 574 | 575 | mithril@^1.1.4: 576 | version "1.1.6" 577 | resolved "https://registry.yarnpkg.com/mithril/-/mithril-1.1.6.tgz#bd2cc0de3d3c86076a6a7a30367a601a1bd108f3" 578 | 579 | module-deps@^4.0.8: 580 | version "4.1.1" 581 | resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd" 582 | dependencies: 583 | JSONStream "^1.0.3" 584 | browser-resolve "^1.7.0" 585 | cached-path-relative "^1.0.0" 586 | concat-stream "~1.5.0" 587 | defined "^1.0.0" 588 | detective "^4.0.0" 589 | duplexer2 "^0.1.2" 590 | inherits "^2.0.1" 591 | parents "^1.0.0" 592 | readable-stream "^2.0.2" 593 | resolve "^1.1.3" 594 | stream-combiner2 "^1.1.1" 595 | subarg "^1.0.0" 596 | through2 "^2.0.0" 597 | xtend "^4.0.0" 598 | 599 | once@^1.3.0: 600 | version "1.4.0" 601 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 602 | dependencies: 603 | wrappy "1" 604 | 605 | os-browserify@~0.3.0: 606 | version "0.3.0" 607 | resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" 608 | 609 | pako@~1.0.5: 610 | version "1.0.6" 611 | resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" 612 | 613 | parents@^1.0.0, parents@^1.0.1: 614 | version "1.0.1" 615 | resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" 616 | dependencies: 617 | path-platform "~0.11.15" 618 | 619 | parse-asn1@^5.0.0: 620 | version "5.1.1" 621 | resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" 622 | dependencies: 623 | asn1.js "^4.0.0" 624 | browserify-aes "^1.0.0" 625 | create-hash "^1.1.0" 626 | evp_bytestokey "^1.0.0" 627 | pbkdf2 "^3.0.3" 628 | 629 | path-browserify@~0.0.0: 630 | version "0.0.0" 631 | resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" 632 | 633 | path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: 634 | version "1.0.1" 635 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 636 | 637 | path-parse@^1.0.5: 638 | version "1.0.5" 639 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" 640 | 641 | path-platform@~0.11.15: 642 | version "0.11.15" 643 | resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" 644 | 645 | pbkdf2@^3.0.3: 646 | version "3.0.14" 647 | resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" 648 | dependencies: 649 | create-hash "^1.1.2" 650 | create-hmac "^1.1.4" 651 | ripemd160 "^2.0.1" 652 | safe-buffer "^5.0.1" 653 | sha.js "^2.4.8" 654 | 655 | prettier@^1.11.1: 656 | version "1.12.1" 657 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" 658 | 659 | process-nextick-args@~1.0.6: 660 | version "1.0.7" 661 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 662 | 663 | process-nextick-args@~2.0.0: 664 | version "2.0.0" 665 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" 666 | 667 | process@~0.11.0: 668 | version "0.11.10" 669 | resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" 670 | 671 | public-encrypt@^4.0.0: 672 | version "4.0.2" 673 | resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994" 674 | dependencies: 675 | bn.js "^4.1.0" 676 | browserify-rsa "^4.0.0" 677 | create-hash "^1.1.0" 678 | parse-asn1 "^5.0.0" 679 | randombytes "^2.0.1" 680 | 681 | punycode@1.3.2: 682 | version "1.3.2" 683 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" 684 | 685 | punycode@^1.3.2, punycode@^1.4.1: 686 | version "1.4.1" 687 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 688 | 689 | querystring-es3@~0.2.0: 690 | version "0.2.1" 691 | resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" 692 | 693 | querystring@0.2.0: 694 | version "0.2.0" 695 | resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" 696 | 697 | randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: 698 | version "2.0.6" 699 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" 700 | dependencies: 701 | safe-buffer "^5.1.0" 702 | 703 | randomfill@^1.0.3: 704 | version "1.0.4" 705 | resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" 706 | dependencies: 707 | randombytes "^2.0.5" 708 | safe-buffer "^5.1.0" 709 | 710 | read-only-stream@^2.0.0: 711 | version "2.0.0" 712 | resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" 713 | dependencies: 714 | readable-stream "^2.0.2" 715 | 716 | readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3: 717 | version "2.3.6" 718 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 719 | dependencies: 720 | core-util-is "~1.0.0" 721 | inherits "~2.0.3" 722 | isarray "~1.0.0" 723 | process-nextick-args "~2.0.0" 724 | safe-buffer "~5.1.1" 725 | string_decoder "~1.1.1" 726 | util-deprecate "~1.0.1" 727 | 728 | readable-stream@~2.0.0: 729 | version "2.0.6" 730 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" 731 | dependencies: 732 | core-util-is "~1.0.0" 733 | inherits "~2.0.1" 734 | isarray "~1.0.0" 735 | process-nextick-args "~1.0.6" 736 | string_decoder "~0.10.x" 737 | util-deprecate "~1.0.1" 738 | 739 | resolve@1.1.7: 740 | version "1.1.7" 741 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" 742 | 743 | resolve@^1.1.3, resolve@^1.1.4: 744 | version "1.7.1" 745 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" 746 | dependencies: 747 | path-parse "^1.0.5" 748 | 749 | ripemd160@^2.0.0, ripemd160@^2.0.1: 750 | version "2.0.2" 751 | resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" 752 | dependencies: 753 | hash-base "^3.0.0" 754 | inherits "^2.0.1" 755 | 756 | safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 757 | version "5.1.1" 758 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 759 | 760 | sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: 761 | version "2.4.11" 762 | resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" 763 | dependencies: 764 | inherits "^2.0.1" 765 | safe-buffer "^5.0.1" 766 | 767 | shasum@^1.0.0: 768 | version "1.0.2" 769 | resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" 770 | dependencies: 771 | json-stable-stringify "~0.0.0" 772 | sha.js "~2.4.4" 773 | 774 | shell-quote@^1.6.1: 775 | version "1.6.1" 776 | resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" 777 | dependencies: 778 | array-filter "~0.0.0" 779 | array-map "~0.0.0" 780 | array-reduce "~0.0.0" 781 | jsonify "~0.0.0" 782 | 783 | source-map@~0.5.3: 784 | version "0.5.7" 785 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" 786 | 787 | stream-browserify@^2.0.0: 788 | version "2.0.1" 789 | resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" 790 | dependencies: 791 | inherits "~2.0.1" 792 | readable-stream "^2.0.2" 793 | 794 | stream-combiner2@^1.1.1: 795 | version "1.1.1" 796 | resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" 797 | dependencies: 798 | duplexer2 "~0.1.0" 799 | readable-stream "^2.0.2" 800 | 801 | stream-http@^2.0.0: 802 | version "2.8.1" 803 | resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.1.tgz#d0441be1a457a73a733a8a7b53570bebd9ef66a4" 804 | dependencies: 805 | builtin-status-codes "^3.0.0" 806 | inherits "^2.0.1" 807 | readable-stream "^2.3.3" 808 | to-arraybuffer "^1.0.0" 809 | xtend "^4.0.0" 810 | 811 | stream-splicer@^2.0.0: 812 | version "2.0.0" 813 | resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83" 814 | dependencies: 815 | inherits "^2.0.1" 816 | readable-stream "^2.0.2" 817 | 818 | string_decoder@~0.10.x: 819 | version "0.10.31" 820 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 821 | 822 | string_decoder@~1.0.0: 823 | version "1.0.3" 824 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 825 | dependencies: 826 | safe-buffer "~5.1.0" 827 | 828 | string_decoder@~1.1.1: 829 | version "1.1.1" 830 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" 831 | dependencies: 832 | safe-buffer "~5.1.0" 833 | 834 | subarg@^1.0.0: 835 | version "1.0.0" 836 | resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" 837 | dependencies: 838 | minimist "^1.1.0" 839 | 840 | syntax-error@^1.1.1: 841 | version "1.4.0" 842 | resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" 843 | dependencies: 844 | acorn-node "^1.2.0" 845 | 846 | through2@^2.0.0: 847 | version "2.0.3" 848 | resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" 849 | dependencies: 850 | readable-stream "^2.1.5" 851 | xtend "~4.0.1" 852 | 853 | "through@>=2.2.7 <3": 854 | version "2.3.8" 855 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 856 | 857 | timers-browserify@^1.0.1: 858 | version "1.4.2" 859 | resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" 860 | dependencies: 861 | process "~0.11.0" 862 | 863 | tldjs@^2.3.1: 864 | version "2.3.1" 865 | resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039" 866 | dependencies: 867 | punycode "^1.4.1" 868 | 869 | to-arraybuffer@^1.0.0: 870 | version "1.0.1" 871 | resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" 872 | 873 | tty-browserify@~0.0.0: 874 | version "0.0.1" 875 | resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" 876 | 877 | typedarray@^0.0.6, typedarray@~0.0.5: 878 | version "0.0.6" 879 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 880 | 881 | umd@^3.0.0: 882 | version "3.0.3" 883 | resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" 884 | 885 | url@~0.11.0: 886 | version "0.11.0" 887 | resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" 888 | dependencies: 889 | punycode "1.3.2" 890 | querystring "0.2.0" 891 | 892 | util-deprecate@~1.0.1: 893 | version "1.0.2" 894 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 895 | 896 | util@0.10.3, util@~0.10.1: 897 | version "0.10.3" 898 | resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" 899 | dependencies: 900 | inherits "2.0.1" 901 | 902 | vm-browserify@~0.0.1: 903 | version "0.0.4" 904 | resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" 905 | dependencies: 906 | indexof "0.0.1" 907 | 908 | wrappy@1: 909 | version "1.0.2" 910 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 911 | 912 | xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: 913 | version "4.0.1" 914 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 915 | --------------------------------------------------------------------------------