├── .editorconfig
├── .github
└── ISSUE_TEMPLATE.md
├── .gitignore
├── .version
├── LICENSE
├── Makefile
├── PRIVACY.md
├── README.md
├── images
├── logotype-horizontal.png
├── logotype-horizontal.svg
├── logotype-vertical.png
└── logotype-vertical.svg
└── src
├── Makefile
├── background.js
├── fonts
├── OpenSans-LICENSE.txt
├── OpenSans-Light.ttf
├── OpenSans-Regular.ttf
├── SourceCodePro-LICENSE.txt
└── SourceCodePro-Regular.ttf
├── helpers
├── base.js
├── clipboard.js
├── redraw.js
└── ui.js
├── icon.png
├── icon.svg
├── icon16.png
├── inject.js
├── manifest-chromium.json
├── manifest-firefox.json
├── offscreen
├── offscreen.html
└── offscreen.js
├── options
├── interface.js
├── options.html
├── options.js
└── options.less
├── package.json
├── popup
├── addEditInterface.js
├── colors-dark.less
├── colors-light.less
├── colors.less
├── detailsInterface.js
├── icon-back.svg
├── icon-bs-delete.svg
├── icon-copy.svg
├── icon-delete.svg
├── icon-details.svg
├── icon-edit.svg
├── icon-generate.svg
├── icon-history.svg
├── icon-key.svg
├── icon-save.svg
├── icon-search.svg
├── icon-user.svg
├── interface.js
├── layoutInterface.js
├── modalDialog.js
├── models
│ ├── Login.js
│ ├── Settings.js
│ └── Tree.js
├── notifications.js
├── page-loader-dark.gif
├── page-loader.gif
├── popup.html
├── popup.js
├── popup.less
└── searchinterface.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | max_line_length = 100
11 |
12 | [Makefile]
13 | indent_style = tab
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### General information
2 |
3 |
4 |
5 | - Operating system + version:
6 | - Browser + version:
7 | - Information about the host app:
8 | - How did you install it?
9 |
10 | - If installed an official release, put a version (`$ browserpass --version`):
11 | - If built from sources, put a commit id (`$ git describe --always`):
12 | - Information about the browser extension:
13 | - How did you install it?
14 |
15 | - Browserpass extension version as reported by your browser:
16 |
17 | ---
18 |
19 | If you are getting an error immediately after opening popup, have you followed the [Configure browsers](https://github.com/browserpass/browserpass-native#configure-browsers) documentation section?
20 |
21 | ---
22 |
23 | ### Exact steps to reproduce the problem
24 |
25 | 1.
26 |
27 | 2.
28 |
29 | 3.
30 |
31 | ### What should happen?
32 |
33 | ### What happened instead?
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /chromium
2 | /firefox
3 | /dist
4 | /dist-webstore
5 |
6 | /src/node_modules
7 | /src/css
8 | /src/js
9 | /src/*.log
10 |
11 | *.pem
12 | *.crx
13 | /.vscode
14 |
--------------------------------------------------------------------------------
/.version:
--------------------------------------------------------------------------------
1 | 3.10.2
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2018-2021, Maxim Baz & Steve Gilberd
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | VERSION ?= $(shell cat .version)
2 |
3 | CLEAN_FILES := chromium firefox dist dist-webstore
4 | CHROME := $(shell which chromium 2>/dev/null || which chromium-browser 2>/dev/null || which chrome 2>/dev/null || which google-chrome 2>/dev/null || which google-chrome-stable 2>/dev/null)
5 |
6 | #######################
7 | # For local development
8 |
9 | .PHONY: all
10 | all: extension chromium firefox
11 |
12 | .PHONY: extension
13 | extension:
14 | $(MAKE) -C src
15 |
16 | EXTENSION_FILES := \
17 | src/*.png \
18 | src/*.svg \
19 | src/fonts/* \
20 | src/popup/*.html \
21 | src/popup/*.gif \
22 | src/popup/*.svg \
23 | src/offscreen/*.html \
24 | src/options/*.html
25 | EXTENSION_FILES := \
26 | $(wildcard $(EXTENSION_FILES)) \
27 | src/css/popup.dist.css \
28 | src/css/options.dist.css \
29 | src/js/background.dist.js \
30 | src/js/popup.dist.js \
31 | src/js/offscreen.dist.js \
32 | src/js/options.dist.js \
33 | src/js/inject.dist.js
34 | CHROMIUM_FILES := $(patsubst src/%,chromium/%, $(EXTENSION_FILES))
35 | FIREFOX_FILES := $(patsubst src/%,firefox/%, $(EXTENSION_FILES))
36 |
37 | .PHONY: chromium
38 | chromium: extension $(CHROMIUM_FILES) chromium/manifest.json
39 |
40 | $(CHROMIUM_FILES) : chromium/% : src/%
41 | [ -d $(dir $@) ] || mkdir -p $(dir $@)
42 | cp $< $@
43 |
44 | chromium/manifest.json : src/manifest-chromium.json
45 | [ -d $(dir $@) ] || mkdir -p $(dir $@)
46 | cp $< $@
47 |
48 | .PHONY: firefox
49 | firefox: extension $(FIREFOX_FILES) firefox/manifest.json
50 |
51 | $(FIREFOX_FILES) : firefox/% : src/%
52 | [ -d $(dir $@) ] || mkdir -p $(dir $@)
53 | cp $< $@
54 |
55 | firefox/manifest.json : src/manifest-firefox.json
56 | [ -d $(dir $@) ] || mkdir -p $(dir $@)
57 | cp $< $@
58 |
59 | #######################
60 | # For official releases
61 |
62 | .PHONY: clean
63 | clean:
64 | rm -rf $(CLEAN_FILES)
65 | $(MAKE) -C src clean
66 |
67 | .PHONY: crx-webstore
68 | crx-webstore:
69 | "$(CHROME)" --disable-gpu --pack-extension=./chromium --pack-extension-key=webstore.pem
70 | mv chromium.crx browserpass-webstore.crx
71 |
72 | .PHONY: crx-github
73 | crx-github:
74 | "$(CHROME)" --disable-gpu --pack-extension=./chromium --pack-extension-key=github.pem
75 | mv chromium.crx browserpass-github.crx
76 |
77 | .PHONY: dist
78 | dist: clean extension chromium firefox crx-webstore crx-github
79 | mkdir -p dist
80 |
81 | git -c tar.tar.gz.command="gzip -cn" archive -o dist/browserpass-extension-$(VERSION).tar.gz --format tar.gz --prefix=browserpass-extension-$(VERSION)/ $(VERSION)
82 |
83 | (cd chromium && zip -r ../dist/browserpass-chromium-$(VERSION).zip *)
84 | (cd firefox && zip -r ../dist/browserpass-firefox-$(VERSION).zip *)
85 |
86 | mv browserpass-webstore.crx dist/browserpass-webstore-$(VERSION).crx
87 | mv browserpass-github.crx dist/browserpass-github-$(VERSION).crx
88 |
89 | for file in dist/*; do \
90 | gpg --detach-sign --armor "$$file"; \
91 | done
92 |
93 | mkdir -p dist-webstore
94 |
95 | cp dist/browserpass-firefox-$(VERSION).zip dist-webstore/firefox-$(VERSION).zip
96 | mv dist/browserpass-extension-$(VERSION).tar.gz dist-webstore/firefox-$(VERSION)-src.tar.gz
97 |
98 | cp -a chromium dist-webstore/
99 | sed -i '/"key"/d' dist-webstore/chromium/manifest.json
100 | (cd dist-webstore/chromium && zip -r ../chrome-$(VERSION).zip *)
101 | rm -rf dist-webstore/chromium
102 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | Browserpass Privacy Policy
2 | ==========================
3 |
4 | ## Definitions
5 |
6 | - Browserpass means the WebExtension at https://github.com/browserpass/browserpass-extension
7 | - Browserpass OTP means the WebExtension at https://github.com/browserpass/browserpass-otp
8 | - User means the user of the web browser where Browserpass or Browserpass OTP is installed.
9 | - Password Store means one or more locations on disk where the user stores encrypted credential files.
10 | - Credential File(s) means the individual credential files in the User's password store.
11 | - Developer(s) means the individuals who are responsible for the development of Browserpass and Browserpass OTP.
12 |
13 | ## Applicability
14 |
15 | This Privacy Policy applies to Browserpass and Browserpass OTP.
16 |
17 | ## Usage of Credential Files
18 |
19 | During the course of normal operation, Browserpass handles decrypted Credential Files.
20 | Only files selected by the User via the Browserpass interface are decrypted.
21 |
22 | The contents of decrypted Credential Files are used *only* for the following purposes:
23 |
24 | - To copy login credentials to the clipboard;
25 | - To automatically fill login credentials into a website in the current tab;
26 | - To provide the User with an interface to edit the contents of a selected Credential File,
27 | - To provide the OTP seed to Browserpass OTP
28 | - To fill other fields as requested by the User (e.g. credit card data)
29 |
30 | ## Use & Transmission of Data
31 |
32 | Browserpass will fill data selected by the User to the website in the currently
33 | active browser tab. This implies that data will be sent to that site when the
34 | form into which the data has been filled is submitted.
35 |
36 | If the form fields detected by Browserpass belong to a foreign origin, Browserpass
37 | will prompt the User to confirm whether they would like to continue filling those
38 | fields.
39 |
40 | If an OTP seed is detected in a credential file when it is decrypted, it will be
41 | passed to Browserpass OTP.
42 |
43 | Browserpass only holds the decrypted contents of Credential Files while they are
44 | actively being used by the User. Once the action selected by the User has been
45 | completed, the data becomes out of scope, and will be cleaned up by the browser's
46 | garbage collection mechanism.
47 |
48 | Browserpass contains an autosubmit feature, which defaults to disabled. If enabled by
49 | the user, this will cause Browserpass to automatically submit the form into which
50 | credentials were filled immediately after filling. The Developers do not recommend
51 | use of this feature, and it will never be enabled by default.
52 |
53 | Browserpass OTP will, upon receipt of an OTP seed from Browserpass, generate an OTP
54 | code and make it available on demand via the Browserpass OTP popup interface. If
55 | Browserpass is not already using the clipboard, it will also place that code on the
56 | clipboard.
57 |
58 | Browserpass OTP will retain the OTP seed until the tab for which the seed applies is
59 | navigated to a different origin, so that it can generate new codes as needed (typically
60 | every 30 seconds).
61 |
62 | IN NO EVENT WILL BROWSERPASS OR BROWSERPASS OTP EVER SEND DATA OF ANY KIND TO ANY PARTY
63 | OTHER THAN A WEBSITE INTO INTO WHICH THE USER HAS DELIBERATELY REQUESTED BROWSERPASS
64 | TO FILL DATA.
65 |
66 | ## Security of Transmission
67 |
68 | Filled content will be submitted via whatever mechanism is provided by the form that
69 | has been filled. This is determined by the website to which the form belongs. For clarity,
70 | please note that some sites do not properly secure such forms - Browserpass will prompt
71 | the User before filling data into any non-https origin.
72 |
73 | Some websites may use a secure origin, but transmit data via insecure means. It is possible
74 | that Browserpass may not be able to detect all such sites, so filling and submitting
75 | data is done solely at the User's own risk.
76 |
77 | ## Local Storage
78 |
79 | Browserpass may store the following via the browser's local storage API:
80 |
81 | - Historical usage data, in order to sort the list of Credential Files in the Browserpass
82 | popup interface by recency and usage count.
83 | - Usage of any given Credential File on an origin that cannot be automatically matched.
84 | - Responses to confirmation prompts.
85 |
86 | Local storage may be cleared via the Browserpass options screen.
87 |
88 | Decrypted contents of Credential Files are never placed in local storage for any reason.
89 |
90 | ## Further Detail
91 |
92 | For further detail on how Browserpass functions and protects your data, please see the
93 | readme at https://github.com/browserpass/browserpass-extension/blob/master/README.md.
94 |
95 | ## Liability
96 |
97 | THE SOFTWARE IS PROVIDED "AS IS" AND THE DEVELOPERS DISCLAIM ALL WARRANTIES
98 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
99 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE DEVELOPERS BE LIABLE FOR
100 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
101 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
102 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
103 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
104 |
--------------------------------------------------------------------------------
/images/logotype-horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/images/logotype-horizontal.png
--------------------------------------------------------------------------------
/images/logotype-vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/images/logotype-vertical.png
--------------------------------------------------------------------------------
/src/Makefile:
--------------------------------------------------------------------------------
1 | BROWSERIFY := node_modules/.bin/browserify
2 | PRETTIER := node_modules/.bin/prettier
3 | LESSC := node_modules/.bin/lessc
4 |
5 | CLEAN_FILES := css js
6 | PRETTIER_FILES := $(wildcard *.json *.js popup/*.js offscreen/*.js options/*.js *.less popup/*.less options/*.less *.html popup/*.html offscreen/*.html options/*.html)
7 |
8 | .PHONY: all
9 | all: deps prettier css/popup.dist.css css/options.dist.css js/background.dist.js js/popup.dist.js js/offscreen.dist.js js/options.dist.js js/inject.dist.js
10 |
11 | .PHONY: deps
12 | deps:
13 | yarn install
14 |
15 | .PHONY: prettier
16 | prettier: $(PRETTIER) $(PRETTIER_FILES)
17 | $(PRETTIER) --write $(PRETTIER_FILES)
18 |
19 | css/popup.dist.css: $(LESSC) popup/*.less
20 | [ -d css ] || mkdir -p css
21 | $(LESSC) popup/popup.less css/popup.dist.css
22 |
23 | css/options.dist.css: $(LESSC) options/*.less
24 | [ -d css ] || mkdir -p css
25 | $(LESSC) options/options.less css/options.dist.css
26 |
27 | js/background.dist.js: $(BROWSERIFY) background.js helpers/*.js
28 | [ -d js ] || mkdir -p js
29 | $(BROWSERIFY) -o js/background.dist.js background.js
30 |
31 | js/popup.dist.js: $(BROWSERIFY) popup/*.js helpers/*.js
32 | [ -d js ] || mkdir -p js
33 | $(BROWSERIFY) -o js/popup.dist.js popup/popup.js
34 |
35 | js/offscreen.dist.js: $(BROWSERIFY) offscreen/*.js helpers/*.js
36 | [ -d js ] || mkdir -p js
37 | $(BROWSERIFY) -o js/offscreen.dist.js offscreen/offscreen.js
38 |
39 | js/options.dist.js: $(BROWSERIFY) options/*.js
40 | [ -d js ] || mkdir -p js
41 | $(BROWSERIFY) -o js/options.dist.js options/options.js
42 |
43 | js/inject.dist.js: $(BROWSERIFY) inject.js
44 | [ -d js ] || mkdir -p js
45 | $(BROWSERIFY) -o js/inject.dist.js inject.js
46 |
47 | # Firefox requires the last command to evaluate to something serializable
48 | # https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/executeScript#Return_value
49 | echo ";undefined" >> js/inject.dist.js
50 |
51 | .PHONY: clean
52 | clean:
53 | rm -rf $(CLEAN_FILES)
54 |
--------------------------------------------------------------------------------
/src/fonts/OpenSans-LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/src/fonts/OpenSans-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/src/fonts/OpenSans-Light.ttf
--------------------------------------------------------------------------------
/src/fonts/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/src/fonts/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/src/fonts/SourceCodePro-LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/src/fonts/SourceCodePro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/src/fonts/SourceCodePro-Regular.ttf
--------------------------------------------------------------------------------
/src/helpers/base.js:
--------------------------------------------------------------------------------
1 | //------------------------------------- Initialisation --------------------------------------//
2 | "use strict";
3 |
4 | const FuzzySort = require("fuzzysort");
5 | const sha1 = require("sha1");
6 | const ignore = require("ignore");
7 | const hash = require("hash.js");
8 | const Authenticator = require("otplib").authenticator.Authenticator;
9 | const BrowserpassURL = require("@browserpass/url");
10 |
11 | const fieldsPrefix = {
12 | secret: ["secret", "password", "pass"],
13 | login: ["login", "username", "user"],
14 | openid: ["openid"],
15 | otp: ["otp", "totp"],
16 | url: ["url", "uri", "website", "site", "link", "launch"],
17 | };
18 |
19 | const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u");
20 | const LATEST_NATIVE_APP_VERSION = 3001000;
21 | const AUTH_URL_QUERY_PARAM = "authUrl";
22 | const LAUNCH_URL_DEPRECATION_MESSAGE = `
Deprecation
"Ctrl+G" and "Ctrl+Shift+G" shortcuts are deprecated and will be removed in a future version.
It is no longer necessary to open websites which require basic auth using these shortcuts. Navigate websites normally and Browserpass will automatically open a popup for you to choose the credentials.
`;
23 |
24 | module.exports = {
25 | containsSymbolsRegEx,
26 | fieldsPrefix,
27 | AUTH_URL_QUERY_PARAM,
28 | LATEST_NATIVE_APP_VERSION,
29 | LAUNCH_URL_DEPRECATION_MESSAGE,
30 | deepCopy,
31 | filterSortLogins,
32 | getPopupUrl,
33 | getSetting,
34 | ignoreFiles,
35 | isChrome,
36 | makeTOTP,
37 | parseAuthUrl,
38 | prepareLogin,
39 | prepareLogins,
40 | unsecureRequestWarning,
41 | };
42 |
43 | //----------------------------------- Function definitions ----------------------------------//
44 |
45 | /**
46 | * Deep copy an object
47 | *
48 | * Firefox requires data to be serializable,
49 | * this removes everything offending such as functions
50 | *
51 | * @since 3.0.0 moved to helpers.js 3.8.0
52 | *
53 | * @param object obj an object to copy
54 | * @return object a new deep copy
55 | */
56 | function deepCopy(obj) {
57 | return JSON.parse(JSON.stringify(obj));
58 | }
59 |
60 | /**
61 | * Returns url string of the html popup page
62 | * @since 3.10.0
63 | *
64 | * @returns string
65 | */
66 | function getPopupUrl() {
67 | const base = chrome || browser;
68 | return base.runtime.getURL("popup/popup.html");
69 | }
70 |
71 | /*
72 | * Get most relevant setting value
73 | *
74 | * @param string key Setting key
75 | * @param object login Login object
76 | * @param object settings Settings object
77 | * @return object Setting value
78 | */
79 | function getSetting(key, login, settings) {
80 | if (typeof login.settings[key] !== "undefined") {
81 | return login.settings[key];
82 | }
83 | if (typeof settings.stores[login.store.id].settings[key] !== "undefined") {
84 | return settings.stores[login.store.id].settings[key];
85 | }
86 |
87 | return settings[key];
88 | }
89 |
90 | /**
91 | * returns true if agent string is Chrome / Chromium
92 | *
93 | * @since 3.10.0
94 | */
95 | function isChrome() {
96 | return chrome.runtime.getURL("/").startsWith("chrom");
97 | }
98 |
99 | /**
100 | * Get the deepest available domain component of a path
101 | *
102 | * @since 3.2.3
103 | *
104 | * @param string path Path to parse
105 | * @param object currentHost Current host info for the active tab
106 | * @return object|null Extracted domain info
107 | */
108 | function pathToInfo(path, currentHost) {
109 | var parts = path.split(/\//).reverse();
110 | for (var key in parts) {
111 | if (parts[key].indexOf("@") >= 0) {
112 | continue;
113 | }
114 | var info = BrowserpassURL.parseHost(parts[key]);
115 |
116 | // Part is considered to be a domain component in one of the following cases:
117 | // - it is a valid domain with well-known TLD (github.com, login.github.com)
118 | // - it is or isn't a valid domain with any TLD but the current host matches it EXACTLY (localhost, pi.hole)
119 | // - it is or isn't a valid domain with any TLD but the current host is its subdomain (login.pi.hole)
120 | if (
121 | info.validDomain ||
122 | currentHost.hostname === info.hostname ||
123 | currentHost.hostname.endsWith(`.${info.hostname}`)
124 | ) {
125 | return info;
126 | }
127 | }
128 |
129 | return null;
130 | }
131 |
132 | /**
133 | * Returns decoded url param for "authUrl" if present
134 | * @since 3.10.0
135 | * @param string url string to parse and compare against extension popup url
136 | * @returns string | null
137 | */
138 | function parseAuthUrl(url) {
139 | const currentUrl = url || null;
140 |
141 | // query url not exact match when includes fragments, so must start with extension url
142 | if (currentUrl && `${currentUrl}`.startsWith(getPopupUrl())) {
143 | const encodedUrl = new URL(currentUrl).searchParams.get(AUTH_URL_QUERY_PARAM);
144 | if (encodedUrl) {
145 | return decodeURIComponent(encodedUrl);
146 | }
147 | }
148 | return null;
149 | }
150 |
151 |
152 | /**
153 | * Prepare list of logins based on provided files
154 | *
155 | * @since 3.1.0
156 | *
157 | * @param string array List of password files
158 | * @param string object Settings object
159 | * @return array List of logins
160 | */
161 | function prepareLogins(files, settings) {
162 | const logins = [];
163 | let index = 0;
164 | let origin = new BrowserpassURL(
165 | settings.authRequested ? parseAuthUrl(settings.tab.url) : settings.origin
166 | );
167 |
168 | for (let storeId in files) {
169 | for (let key in files[storeId]) {
170 | // set login fields
171 | const login = prepareLogin(settings, storeId, files[storeId][key], index++, origin);
172 |
173 | logins.push(login);
174 | }
175 | }
176 |
177 | return logins;
178 | }
179 |
180 | /**
181 | * Prepare a single login based settings, storeId, and path
182 | *
183 | * @since 3.8.0
184 | *
185 | * @param string settings Settings object
186 | * @param string storeId Store ID alphanumeric ID
187 | * @param string file Relative path in store to password
188 | * @param number index An array index for login, if building an array of logins (optional, default: 0)
189 | * @param object origin Instance of BrowserpassURL (optional, default: new BrowserpassURL(settings.origin))
190 | * @return object of login
191 | */
192 | function prepareLogin(settings, storeId, file, index = 0, origin = undefined) {
193 | const login = {
194 | index: index > -1 ? parseInt(index) : 0,
195 | store: settings.stores[storeId],
196 | // remove the file-type extension
197 | login: file.replace(/\.[^.]+$/u, ""),
198 | loginPath: file,
199 | allowFill: true,
200 | };
201 |
202 | origin = BrowserpassURL.prototype.isPrototypeOf(origin)
203 | ? origin
204 | : new BrowserpassURL(settings.origin);
205 |
206 | // extract url info from path
207 | let pathInfo = pathToInfo(storeId + "/" + login.login, origin);
208 | if (pathInfo) {
209 | // set assumed host
210 | login.host = pathInfo.port ? `${pathInfo.hostname}:${pathInfo.port}` : pathInfo.hostname;
211 |
212 | // check whether extracted path info matches the current origin
213 | login.inCurrentHost = origin.hostname === pathInfo.hostname;
214 |
215 | // check whether the current origin is subordinate to extracted path info, meaning:
216 | // - that the path info is not a single level domain (e.g. com, net, local)
217 | // - and that the current origin is a subdomain of that path info
218 | if (pathInfo.hostname.includes(".") && origin.hostname.endsWith(`.${pathInfo.hostname}`)) {
219 | login.inCurrentHost = true;
220 | }
221 |
222 | // filter out entries with a non-matching port
223 | if (pathInfo.port && pathInfo.port !== origin.port) {
224 | login.inCurrentHost = false;
225 | }
226 | } else {
227 | login.host = null;
228 | login.inCurrentHost = false;
229 | }
230 |
231 | // update recent counter
232 | login.recent =
233 | settings.recent[sha1(settings.origin + sha1(login.store.id + sha1(login.login)))];
234 | if (!login.recent) {
235 | login.recent = {
236 | when: 0,
237 | count: 0,
238 | };
239 | }
240 |
241 | return login;
242 | }
243 |
244 | /**
245 | * Filter and sort logins
246 | *
247 | * @since 3.1.0
248 | *
249 | * @param string array List of logins
250 | * @param string object Settings object
251 | * @return array Filtered and sorted list of logins
252 | */
253 | function filterSortLogins(logins, searchQuery, currentDomainOnly) {
254 | var fuzzyFirstWord = searchQuery.substr(0, 1) !== " ";
255 | searchQuery = searchQuery.trim();
256 |
257 | // get candidate list
258 | var candidates = logins.map((candidate) => {
259 | let lastSlashIndex = candidate.login.lastIndexOf("/") + 1;
260 | return Object.assign(candidate, {
261 | path: candidate.login.substr(0, lastSlashIndex),
262 | display: candidate.login.substr(lastSlashIndex),
263 | });
264 | });
265 |
266 | var mostRecent = null;
267 | if (currentDomainOnly) {
268 | var recent = candidates.filter(function (login) {
269 | if (login.recent.count > 0) {
270 | // find most recently used login
271 | if (!mostRecent || login.recent.when > mostRecent.recent.when) {
272 | mostRecent = login;
273 | }
274 | return true;
275 | }
276 | return false;
277 | });
278 | var remainingInCurrentDomain = candidates.filter(
279 | (login) => login.inCurrentHost && !login.recent.count
280 | );
281 | candidates = recent.concat(remainingInCurrentDomain);
282 | }
283 |
284 | candidates.sort((a, b) => {
285 | // show most recent first
286 | if (a === mostRecent) {
287 | return -1;
288 | }
289 | if (b === mostRecent) {
290 | return 1;
291 | }
292 |
293 | // sort by frequency
294 | var countDiff = b.recent.count - a.recent.count;
295 | if (countDiff) {
296 | return countDiff;
297 | }
298 |
299 | // sort by specificity, only if filtering for one domain
300 | if (currentDomainOnly) {
301 | var domainLevelsDiff =
302 | (b.login.match(/\./g) || []).length - (a.login.match(/\./g) || []).length;
303 | if (domainLevelsDiff) {
304 | return domainLevelsDiff;
305 | }
306 | }
307 |
308 | // sort alphabetically
309 | return a.login.localeCompare(b.login);
310 | });
311 |
312 | if (searchQuery.length) {
313 | let filter = searchQuery.split(/\s+/);
314 | let fuzzyFilter = fuzzyFirstWord ? filter[0] : "";
315 | let substringFilters = filter.slice(fuzzyFirstWord ? 1 : 0).map((w) => w.toLowerCase());
316 |
317 | // First reduce the list by running the substring search
318 | substringFilters.forEach(function (word) {
319 | candidates = candidates.filter((c) => c.login.toLowerCase().indexOf(word) >= 0);
320 | });
321 |
322 | // Then run the fuzzy filter
323 | let fuzzyResults = {};
324 | if (fuzzyFilter) {
325 | candidates = FuzzySort.go(fuzzyFilter, candidates, {
326 | keys: ["login", "store.name"],
327 | allowTypo: false,
328 | }).map((result) => {
329 | fuzzyResults[result.obj.login] = result;
330 | return result.obj;
331 | });
332 | }
333 |
334 | // Finally highlight all matches
335 | candidates = candidates.map((c) => highlightMatches(c, fuzzyResults, substringFilters));
336 | }
337 |
338 | // Prefix root entries with slash to let them have some visible path
339 | candidates.forEach((c) => {
340 | c.path = c.path || "/";
341 | });
342 |
343 | return candidates;
344 | }
345 |
346 | /**
347 | * Generate TOTP token
348 | *
349 | * @since 3.6.0
350 | *
351 | * @param object params OTP generation params
352 | * @return string Generated OTP code
353 | */
354 | function makeTOTP(params) {
355 | switch (params.algorithm) {
356 | case "sha1":
357 | case "sha256":
358 | case "sha512":
359 | break;
360 | default:
361 | throw new Error(`Unsupported TOTP algorithm: ${params.algorithm}`);
362 | }
363 |
364 | var generator = new Authenticator();
365 | generator.options = {
366 | crypto: {
367 | createHmac: (a, k) => hash.hmac(hash[a], k),
368 | },
369 | algorithm: params.algorithm,
370 | digits: params.digits,
371 | step: params.period,
372 | };
373 |
374 | return generator.generate(params.secret);
375 | }
376 |
377 | //----------------------------------- Private functions ----------------------------------//
378 |
379 | /**
380 | * Highlight filter matches
381 | *
382 | * @since 3.0.0
383 | *
384 | * @param object entry password entry
385 | * @param object fuzzyResults positions of fuzzy filter matches
386 | * @param array substringFilters list of substring filters applied
387 | * @return object entry with highlighted matches
388 | */
389 | function highlightMatches(entry, fuzzyResults, substringFilters) {
390 | // Add all positions of the fuzzy search to the array
391 | let matches = (
392 | fuzzyResults[entry.login] && fuzzyResults[entry.login][0]
393 | ? fuzzyResults[entry.login][0].indexes
394 | : []
395 | ).slice();
396 |
397 | // Add all positions of substring searches to the array
398 | let login = entry.login.toLowerCase();
399 | for (let word of substringFilters) {
400 | let startIndex = login.indexOf(word);
401 | for (let i = 0; i < word.length; i++) {
402 | matches.push(startIndex + i);
403 | }
404 | }
405 |
406 | // Prepare the final array of matches before
407 | matches = sortUnique(matches, (a, b) => a - b);
408 |
409 | const OPEN = "";
410 | const CLOSE = "";
411 | let highlighted = "";
412 | var matchesIndex = 0;
413 | var opened = false;
414 | for (var i = 0; i < entry.login.length; ++i) {
415 | var char = entry.login[i];
416 |
417 | if (i == entry.path.length) {
418 | if (opened) {
419 | highlighted += CLOSE;
420 | }
421 | var path = highlighted;
422 | highlighted = "";
423 | if (opened) {
424 | highlighted += OPEN;
425 | }
426 | }
427 |
428 | if (matches[matchesIndex] === i) {
429 | matchesIndex++;
430 | if (!opened) {
431 | opened = true;
432 | highlighted += OPEN;
433 | }
434 | } else {
435 | if (opened) {
436 | opened = false;
437 | highlighted += CLOSE;
438 | }
439 | }
440 | highlighted += char;
441 | }
442 | if (opened) {
443 | opened = false;
444 | highlighted += CLOSE;
445 | }
446 | let display = highlighted;
447 |
448 | return Object.assign(entry, {
449 | path: path,
450 | display: display,
451 | });
452 | }
453 |
454 | /**
455 | * Filter out ignored files according to .browserpass.json rules
456 | *
457 | * @since 3.2.0
458 | *
459 | * @param object files Arrays of files, grouped by store
460 | * @param object settings Settings object
461 | * @return object Filtered arrays of files, grouped by store
462 | */
463 | function ignoreFiles(files, settings) {
464 | let filteredFiles = {};
465 | for (let store in files) {
466 | let storeSettings = settings.stores[store].settings;
467 | if (storeSettings.hasOwnProperty("ignore")) {
468 | if (typeof storeSettings.ignore === "string") {
469 | storeSettings.ignore = [storeSettings.ignore];
470 | }
471 | filteredFiles[store] = ignore().add(storeSettings.ignore).filter(files[store]);
472 | } else {
473 | filteredFiles[store] = files[store];
474 | }
475 | }
476 | return filteredFiles;
477 | }
478 |
479 | /**
480 | * Sort and remove duplicates
481 | *
482 | * @since 3.0.0
483 | *
484 | * @param array array items to sort
485 | * @param function comparator sort comparator
486 | * @return array sorted items without duplicates
487 | */
488 | function sortUnique(array, comparator) {
489 | return array
490 | .sort(comparator)
491 | .filter((elem, index, arr) => index == !arr.length || arr[index - 1] != elem);
492 | }
493 |
494 | /**
495 | * Returns warning html string with unsecure url specified
496 | * @returns html string
497 | */
498 | function unsecureRequestWarning(url) {
499 | return (
500 | "
Warning: Are you sure you want to do this?
" +
501 | "
You are about to send login credentials via an insecure connection!
" +
502 | "
If there is an attacker watching your network traffic, they may be able to see your username and password.
" +
503 | `
URL: ${url}
`
504 | );
505 | }
506 |
--------------------------------------------------------------------------------
/src/helpers/clipboard.js:
--------------------------------------------------------------------------------
1 | //------------------------------------- Initialization --------------------------------------//
2 | "use strict";
3 |
4 | module.exports = {
5 | readFromClipboard,
6 | writeToClipboard,
7 | };
8 | //----------------------------------- Function definitions ----------------------------------//
9 |
10 | /**
11 | * Read plain text from clipboard
12 | *
13 | * @since 3.2.0
14 | *
15 | * @return string The current plaintext content of the clipboard
16 | */
17 | function readFromClipboard() {
18 | const ta = document.createElement("textarea");
19 | // these lines are carefully crafted to make paste work in both Chrome and Firefox
20 | ta.contentEditable = true;
21 | ta.textContent = "";
22 | document.body.appendChild(ta);
23 | ta.select();
24 | document.execCommand("paste");
25 | const content = ta.value;
26 | document.body.removeChild(ta);
27 | return content;
28 | }
29 |
30 | /**
31 | * Copy text to clipboard and optionally clear it from the clipboard after one minute
32 | *
33 | * @since 3.2.0
34 | *
35 | * @param string text Text to copy
36 | * @return void
37 | */
38 | async function writeToClipboard(text) {
39 | // Error if we received the wrong kind of data.
40 | if (typeof text !== "string") {
41 | throw new TypeError(`Value provided must be a 'string', got '${typeof text}'.`);
42 | }
43 |
44 | document.addEventListener(
45 | "copy",
46 | function (e) {
47 | e.clipboardData.setData("text/plain", text);
48 | e.preventDefault();
49 | },
50 | { once: true }
51 | );
52 | document.execCommand("copy");
53 | }
54 |
--------------------------------------------------------------------------------
/src/helpers/redraw.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const m = require("mithril");
4 |
5 | module.exports = {
6 | increaseModalHeight,
7 | };
8 |
9 | /**
10 | * Increases modal window height using a document child reference height
11 | * Maximum height is 1000px
12 | * @param referenceElement an htmlElement returned from one of document.getElementBy... methods
13 | * @since 3.10.0
14 | */
15 | function increaseModalHeight(referenceElement, heightDiff = 50) {
16 | if (typeof heightDiff != "number") {
17 | heightDiff = 50;
18 | }
19 | if (!referenceElement) {
20 | return;
21 | }
22 | const rootContentEl = document.getRootNode().getElementsByClassName("layout")[0] ?? null;
23 | if (rootContentEl) {
24 | let count = 0;
25 | while (
26 | rootContentEl.clientHeight < 1000 &&
27 | rootContentEl.clientHeight < referenceElement?.clientHeight + heightDiff
28 | ) {
29 | rootContentEl.classList.remove(...rootContentEl.classList);
30 | rootContentEl.classList.add(...["layout", `mh-${count}`]);
31 | m.redraw();
32 | count += 1;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/helpers/ui.js:
--------------------------------------------------------------------------------
1 | //------------------------------------- Initialisation --------------------------------------//
2 | "use strict";
3 |
4 | const m = require("mithril");
5 | const dialog = require("../popup/modalDialog");
6 | const notify = require("../popup/notifications");
7 | const helpers = require("../helpers/base");
8 | const BrowserpassURL = require("@browserpass/url");
9 |
10 | const containsNumbersRegEx = RegExp(/[0-9]/);
11 | const containsSymbolsRegEx = RegExp(/[\p{P}\p{S}]/, "u");
12 |
13 | module.exports = {
14 | handleError,
15 | highlight,
16 | withLogin,
17 | getCurrentUrl
18 | };
19 |
20 | //----------------------------------- Function definitions ----------------------------------//
21 |
22 | /**
23 | * Highlight password characters
24 | *
25 | * @since 3.8.0
26 | *
27 | * @param {string} secret a string to be split by character
28 | * @return {array} mithril vnodes to be rendered
29 | */
30 | function highlight(secret = "") {
31 | return secret.split("").map((c) => {
32 | if (c.match(containsNumbersRegEx)) {
33 | return m("span.char.num", c);
34 | } else if (c.match(containsSymbolsRegEx)) {
35 | return m("span.char.punct", c);
36 | }
37 | return m("span.char", c);
38 | });
39 | }
40 |
41 | /**
42 | * Handle an error
43 | *
44 | * @since 3.0.0
45 | *
46 | * @param Error error Error object
47 | * @param string type Error type
48 | */
49 | function handleError(error, type = "error") {
50 | switch (type) {
51 | case "error":
52 | console.error(error);
53 | // disable error timeout, to allow necessary user action
54 | notify.errorMsg(error.toString(), 0);
55 | break;
56 |
57 | case "warning":
58 | notify.warningMsg(error.toString());
59 | console.warn(error.toString());
60 | break;
61 |
62 | case "success":
63 | notify.successMsg(error.toString());
64 | console.info(error.toString());
65 | break;
66 |
67 | case "info":
68 | default:
69 | notify.infoMsg(error.toString());
70 | console.info(error.toString());
71 | break;
72 | }
73 | }
74 |
75 | /**
76 | * Do a login action
77 | *
78 | * @since 3.0.0
79 | *
80 | * @param string action Action to take
81 | * @param object params Action parameters
82 | * @return void
83 | */
84 | async function withLogin(action, params = {}) {
85 | try {
86 | const url = helpers.parseAuthUrl(this.settings?.tab?.url ?? null);
87 | const askToProceed =
88 | action === "fill" &&
89 | this.settings?.authRequested &&
90 | !params?.confirmedAlready &&
91 | !url?.match(/^https:/i);
92 | if (askToProceed) {
93 | const that = this;
94 | that.doAction = withLogin.bind({
95 | settings: this.settings,
96 | login: this.login,
97 | });
98 | dialog.open(
99 | { message: helpers.unsecureRequestWarning(url), type: "warning" },
100 | function () {
101 | // proceed
102 | params.confirmedAlready = true;
103 | that.doAction(action, params);
104 | }
105 | );
106 | return;
107 | }
108 | } catch (e) {
109 | console.error(e);
110 | }
111 |
112 | try {
113 | switch (action) {
114 | case "fill":
115 | handleError("Filling login details...", "info");
116 | break;
117 | case "launch":
118 | handleError("Launching URL...", "info");
119 | break;
120 | case "launchInNewTab":
121 | handleError("Launching URL in a new tab...", "info");
122 | break;
123 | case "copyPassword":
124 | handleError("Copying password to clipboard...", "info");
125 | break;
126 | case "copyUsername":
127 | handleError("Copying username to clipboard...", "info");
128 | break;
129 | case "copyOTP":
130 | handleError("Copying OTP token to clipboard...", "info");
131 | break;
132 | default:
133 | handleError("Please wait...", "info");
134 | break;
135 | }
136 |
137 | const login = helpers.deepCopy(this.login);
138 |
139 | // hand off action to background script
140 | var response = await chrome.runtime.sendMessage({ action, login, params });
141 | if (response.status != "ok") {
142 | throw new Error(response.message);
143 | } else {
144 | if (response.login && typeof response.login === "object") {
145 | response.login.doAction = withLogin.bind({
146 | settings: this.settings,
147 | login: response.login,
148 | });
149 | } else {
150 | window.close();
151 | }
152 | }
153 | } catch (e) {
154 | handleError(e);
155 | }
156 | }
157 |
158 | /**
159 | * Returns current url
160 | * @param object settings Settings object to use
161 | * @returns object Instance of BrowserpassURL
162 | */
163 | function getCurrentUrl(settings) {
164 | let url;
165 | const authUrl = helpers.parseAuthUrl(window?.location?.href ?? null);
166 | if (settings.authRequested && authUrl) {
167 | url = new BrowserpassURL(authUrl);
168 | } else {
169 | url = new BrowserpassURL(settings.origin);
170 | }
171 | return url
172 | }
173 |
--------------------------------------------------------------------------------
/src/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/src/icon.png
--------------------------------------------------------------------------------
/src/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/src/icon16.png
--------------------------------------------------------------------------------
/src/inject.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const FORM_MARKERS = ["login", "log-in", "log_in", "signin", "sign-in", "sign_in"];
3 | const OPENID_FIELDS = {
4 | selectors: ["input[name*=openid i]", "input[id*=openid i]", "input[class*=openid i]"],
5 | types: ["text"],
6 | };
7 | const USERNAME_FIELDS = {
8 | selectors: [
9 | "input[autocomplete=username i]",
10 |
11 | "input[name=login i]",
12 | "input[name=user i]",
13 | "input[name=username i]",
14 | "input[name=email i]",
15 | "input[name=alias i]",
16 | "input[id=login i]",
17 | "input[id=user i]",
18 | "input[id=username i]",
19 | "input[id=email i]",
20 | "input[id=alias i]",
21 | "input[class=login i]",
22 | "input[class=user i]",
23 | "input[class=username i]",
24 | "input[class=email i]",
25 | "input[class=alias i]",
26 |
27 | "input[name*=login i]",
28 | "input[name*=user i]",
29 | "input[name*=email i]",
30 | "input[name*=alias i]",
31 | "input[id*=login i]",
32 | "input[id*=user i]",
33 | "input[id*=email i]",
34 | "input[id*=alias i]",
35 | "input[class*=login i]",
36 | "input[class*=user i]",
37 | "input[class*=email i]",
38 | "input[class*=alias i]",
39 |
40 | "input[type=email i]",
41 | "input[autocomplete=email i]",
42 | "input[type=text i]",
43 | "input[type=tel i]",
44 | ],
45 | types: ["email", "text", "tel"],
46 | };
47 | const PASSWORD_FIELDS = {
48 | selectors: [
49 | "input[type=password i][autocomplete=current-password i]",
50 | "input[type=password i]",
51 | ],
52 | };
53 | const INPUT_FIELDS = {
54 | selectors: PASSWORD_FIELDS.selectors
55 | .concat(USERNAME_FIELDS.selectors)
56 | .concat(OPENID_FIELDS.selectors),
57 | };
58 | const SUBMIT_FIELDS = {
59 | selectors: [
60 | "[type=submit i]",
61 |
62 | "button[name=login i]",
63 | "button[name=log-in i]",
64 | "button[name=log_in i]",
65 | "button[name=signin i]",
66 | "button[name=sign-in i]",
67 | "button[name=sign_in i]",
68 | "button[id=login i]",
69 | "button[id=log-in i]",
70 | "button[id=log_in i]",
71 | "button[id=signin i]",
72 | "button[id=sign-in i]",
73 | "button[id=sign_in i]",
74 | "button[class=login i]",
75 | "button[class=log-in i]",
76 | "button[class=log_in i]",
77 | "button[class=signin i]",
78 | "button[class=sign-in i]",
79 | "button[class=sign_in i]",
80 | "input[type=button i][name=login i]",
81 | "input[type=button i][name=log-in i]",
82 | "input[type=button i][name=log_in i]",
83 | "input[type=button i][name=signin i]",
84 | "input[type=button i][name=sign-in i]",
85 | "input[type=button i][name=sign_in i]",
86 | "input[type=button i][id=login i]",
87 | "input[type=button i][id=log-in i]",
88 | "input[type=button i][id=log_in i]",
89 | "input[type=button i][id=signin i]",
90 | "input[type=button i][id=sign-in i]",
91 | "input[type=button i][id=sign_in i]",
92 | "input[type=button i][class=login i]",
93 | "input[type=button i][class=log-in i]",
94 | "input[type=button i][class=log_in i]",
95 | "input[type=button i][class=signin i]",
96 | "input[type=button i][class=sign-in i]",
97 | "input[type=button i][class=sign_in i]",
98 |
99 | "button[name*=login i]",
100 | "button[name*=log-in i]",
101 | "button[name*=log_in i]",
102 | "button[name*=signin i]",
103 | "button[name*=sign-in i]",
104 | "button[name*=sign_in i]",
105 | "button[id*=login i]",
106 | "button[id*=log-in i]",
107 | "button[id*=log_in i]",
108 | "button[id*=signin i]",
109 | "button[id*=sign-in i]",
110 | "button[id*=sign_in i]",
111 | "button[class*=login i]",
112 | "button[class*=log-in i]",
113 | "button[class*=log_in i]",
114 | "button[class*=signin i]",
115 | "button[class*=sign-in i]",
116 | "button[class*=sign_in i]",
117 | "input[type=button i][name*=login i]",
118 | "input[type=button i][name*=log-in i]",
119 | "input[type=button i][name*=log_in i]",
120 | "input[type=button i][name*=signin i]",
121 | "input[type=button i][name*=sign-in i]",
122 | "input[type=button i][name*=sign_in i]",
123 | "input[type=button i][id*=login i]",
124 | "input[type=button i][id*=log-in i]",
125 | "input[type=button i][id*=log_in i]",
126 | "input[type=button i][id*=signin i]",
127 | "input[type=button i][id*=sign-in i]",
128 | "input[type=button i][id*=sign_in i]",
129 | "input[type=button i][class*=login i]",
130 | "input[type=button i][class*=log-in i]",
131 | "input[type=button i][class*=log_in i]",
132 | "input[type=button i][class*=signin i]",
133 | "input[type=button i][class*=sign-in i]",
134 | "input[type=button i][class*=sign_in i]",
135 | ],
136 | };
137 |
138 | /**
139 | * Fill password
140 | *
141 | * @since 3.0.0
142 | *
143 | * @param object request Form fill request
144 | * @return object result of filling a form
145 | */
146 | function fillLogin(request) {
147 | var result = {
148 | filledFields: [],
149 | foreignFill: undefined,
150 | };
151 |
152 | // get the login form
153 | let loginForm = undefined;
154 | if (request.fields.includes("openid")) {
155 | // this is an attempt to fill a form containing only openid field
156 | loginForm = form(OPENID_FIELDS);
157 | } else {
158 | // this is an attempt to fill a regular login form
159 | loginForm = form(INPUT_FIELDS);
160 | }
161 |
162 | // don't attempt to fill non-secret forms unless non-secret filling is allowed
163 | if (!request.allowNoSecret && !find(PASSWORD_FIELDS, loginForm)) {
164 | return result;
165 | }
166 |
167 | // ensure the origin is the same, or ask the user for permissions to continue
168 | if (window.location.origin !== request.origin) {
169 | if (!request.allowForeign || request.foreignFills[window.location.origin] === false) {
170 | return result;
171 | }
172 | var message =
173 | "You have requested to fill login credentials into an embedded document from a " +
174 | "different origin than the main document in this tab. Do you wish to proceed?\n\n" +
175 | `Tab origin: ${request.origin}\n` +
176 | `Embedded origin: ${window.location.origin}`;
177 | if (request.foreignFills[window.location.origin] !== true) {
178 | result.foreignOrigin = window.location.origin;
179 | result.foreignFill = confirm(message);
180 | if (!result.foreignFill) {
181 | return result;
182 | }
183 | }
184 | }
185 |
186 | // fill login field
187 | if (
188 | request.fields.includes("login") &&
189 | update(USERNAME_FIELDS, request.login.fields.login, loginForm)
190 | ) {
191 | result.filledFields.push("login");
192 | }
193 |
194 | // fill secret field
195 | if (
196 | request.fields.includes("secret") &&
197 | update(PASSWORD_FIELDS, request.login.fields.secret, loginForm)
198 | ) {
199 | result.filledFields.push("secret");
200 | }
201 |
202 | // fill openid field
203 | if (
204 | request.fields.includes("openid") &&
205 | update(OPENID_FIELDS, request.login.fields.openid, loginForm)
206 | ) {
207 | result.filledFields.push("openid");
208 | }
209 |
210 | // finished filling things successfully
211 | return result;
212 | }
213 |
214 | /**
215 | * Focus submit button, and maybe click on it (based on user settings)
216 | *
217 | * @since 3.0.0
218 | *
219 | * @param object request Form fill request
220 | * @return object result of focusing or submitting a form
221 | */
222 | function focusOrSubmit(request) {
223 | var result = {};
224 |
225 | // get the login form
226 | let loginForm = undefined;
227 | if (request.filledFields.includes("openid")) {
228 | // this is an attempt to focus or submit a form containing only openid field
229 | loginForm = form(OPENID_FIELDS);
230 | } else {
231 | // this is an attempt to focus or submit a regular login form
232 | loginForm = form(INPUT_FIELDS);
233 | }
234 |
235 | // ensure the origin is the same or allowed
236 | if (window.location.origin !== request.origin) {
237 | if (!request.allowForeign || request.foreignFills[window.location.origin] === false) {
238 | return;
239 | }
240 | }
241 |
242 | // check for multiple password fields in the login form
243 | var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, loginForm);
244 | if (password_inputs.length > 1) {
245 | // There is likely a field asking for OTP code, so do not submit form just yet
246 | password_inputs[1].select();
247 | } else {
248 | // try to locate the submit button
249 | var submit = find(SUBMIT_FIELDS, loginForm);
250 |
251 | // Try to submit the form, or focus on the submit button (based on user settings)
252 | if (submit) {
253 | if (request.autoSubmit) {
254 | submit.click();
255 | } else {
256 | submit.focus();
257 | }
258 | } else {
259 | // We need to keep focus somewhere within the form, so that Enter hopefully submits the form.
260 | for (let selectors of [OPENID_FIELDS, PASSWORD_FIELDS, USERNAME_FIELDS]) {
261 | let field = find(selectors, loginForm);
262 | if (field) {
263 | field.focus();
264 | break;
265 | }
266 | }
267 | }
268 | }
269 |
270 | return result;
271 | }
272 |
273 | /**
274 | * Query all visible elements
275 | *
276 | * @since 3.0.0
277 | *
278 | * @param DOMElement parent Parent element to query
279 | * @param object field Field to search for
280 | * @param DOMElement form Search only within this form
281 | * @return array List of search results
282 | */
283 | function queryAllVisible(parent, field, form) {
284 | const result = [];
285 | for (let i = 0; i < field.selectors.length; i++) {
286 | let elems = parent.querySelectorAll(field.selectors[i]);
287 | for (let j = 0; j < elems.length; j++) {
288 | let elem = elems[j];
289 | // Select only elements from specified form
290 | if (form && form != elem.form) {
291 | continue;
292 | }
293 | // Ignore disabled fields
294 | if (elem.disabled) {
295 | continue;
296 | }
297 | // Elem or its parent has a style 'display: none',
298 | // or it is just too narrow to be a real field (a trap for spammers?).
299 | if (elem.offsetWidth < 30 || elem.offsetHeight < 10) {
300 | continue;
301 | }
302 | // We may have a whitelist of acceptable field types. If so, skip elements of a different type.
303 | if (field.types && field.types.indexOf(elem.type.toLowerCase()) < 0) {
304 | continue;
305 | }
306 | // Elem takes space on the screen, but it or its parent is hidden with a visibility style.
307 | let style = window.getComputedStyle(elem);
308 | if (style.visibility == "hidden") {
309 | continue;
310 | }
311 | // Elem is outside of the boundaries of the visible viewport.
312 | let rect = elem.getBoundingClientRect();
313 | if (
314 | rect.x + rect.width < 0 ||
315 | rect.y + rect.height < 0 ||
316 | rect.x > window.innerWidth ||
317 | rect.y > window.innerHeight
318 | ) {
319 | continue;
320 | }
321 | // Elem is hidden by its or or its parent's opacity rules
322 | const OPACITY_LIMIT = 0.1;
323 | let opacity = 1;
324 | for (
325 | let testElem = elem;
326 | opacity >= OPACITY_LIMIT && testElem && testElem.nodeType === Node.ELEMENT_NODE;
327 | testElem = testElem.parentNode
328 | ) {
329 | let style = window.getComputedStyle(testElem);
330 | if (style.opacity) {
331 | opacity *= parseFloat(style.opacity);
332 | }
333 | }
334 | if (opacity < OPACITY_LIMIT) {
335 | continue;
336 | }
337 | // This element is visible, will use it.
338 | result.push(elem);
339 | }
340 | }
341 | return result;
342 | }
343 |
344 | /**
345 | * Query first visible element
346 | *
347 | * @since 3.0.0
348 | *
349 | * @param DOMElement parent Parent element to query
350 | * @param object field Field to search for
351 | * @param DOMElement form Search only within this form
352 | * @return array First search result
353 | */
354 | function queryFirstVisible(parent, field, form) {
355 | var elems = queryAllVisible(parent, field, form);
356 | return elems.length > 0 ? elems[0] : undefined;
357 | }
358 |
359 | /**
360 | * Detect the login form
361 | *
362 | * @since 3.0.0
363 | *
364 | * @param array selectors Selectors to use to find the right form
365 | * @return The login form
366 | */
367 | function form(selectors) {
368 | const elems = queryAllVisible(document, selectors, undefined);
369 | const forms = [];
370 | for (let elem of elems) {
371 | const form = elem.form;
372 | if (form && forms.indexOf(form) < 0) {
373 | forms.push(form);
374 | }
375 | }
376 |
377 | // Try to filter only forms that have some identifying marker
378 | const markedForms = [];
379 | for (let form of forms) {
380 | const props = ["id", "name", "class", "action"];
381 | for (let marker of FORM_MARKERS) {
382 | for (let prop of props) {
383 | let propValue = form.getAttribute(prop) || "";
384 | if (propValue.toLowerCase().indexOf(marker) > -1) {
385 | markedForms.push(form);
386 | }
387 | }
388 | }
389 | }
390 |
391 | // Try to filter only forms that have a password field
392 | const formsWithPassword = [];
393 | for (let form of markedForms) {
394 | if (find(PASSWORD_FIELDS, form)) {
395 | formsWithPassword.push(form);
396 | }
397 | }
398 |
399 | // Give up and return the first available form, if any
400 | if (formsWithPassword.length > 0) {
401 | return formsWithPassword[0];
402 | }
403 | if (markedForms.length > 0) {
404 | return markedForms[0];
405 | }
406 | if (forms.length > 0) {
407 | return forms[0];
408 | }
409 | return undefined;
410 | }
411 |
412 | /**
413 | * Find a form field
414 | *
415 | * @since 3.0.0
416 | *
417 | * @param object field Field to search for
418 | * @param DOMElement form Form to search in
419 | * @return DOMElement First matching form field
420 | */
421 | function find(field, form) {
422 | return queryFirstVisible(document, field, form);
423 | }
424 |
425 | /**
426 | * Update a form field value
427 | *
428 | * @since 3.0.0
429 | *
430 | * @param object field Field to update
431 | * @param string value Value to set
432 | * @param DOMElement form Form for which to set the given field
433 | * @return bool Whether the update succeeded
434 | */
435 | function update(field, value, form) {
436 | if (value === undefined) {
437 | // undefined values should not be filled, but are always considered successful
438 | return true;
439 | }
440 |
441 | if (!value.length) {
442 | return false;
443 | }
444 |
445 | // Focus the input element first
446 | let el = find(field, form);
447 | if (!el) {
448 | return false;
449 | }
450 | for (let eventName of ["click", "focus"]) {
451 | el.dispatchEvent(new Event(eventName, { bubbles: true }));
452 | }
453 |
454 | // Focus may have triggered unveiling a true input, find it again
455 | el = find(field, form);
456 | if (!el) {
457 | return false;
458 | }
459 |
460 | // Focus the potentially new element again
461 | for (let eventName of ["click", "focus"]) {
462 | el.dispatchEvent(new Event(eventName, { bubbles: true }));
463 | }
464 |
465 | // Send some keyboard events indicating that value modification has started (no associated keycode)
466 | for (let eventName of ["keydown", "keypress", "keyup", "input", "change"]) {
467 | el.dispatchEvent(new Event(eventName, { bubbles: true }));
468 | }
469 |
470 | // truncate the value if required by the field
471 | if (el.maxLength > 0) {
472 | value = value.substr(0, el.maxLength);
473 | }
474 |
475 | // Set the field value
476 | let initialValue = el.value || el.getAttribute("value");
477 | el.setAttribute("value", value);
478 | el.value = value;
479 |
480 | // Send the keyboard events again indicating that value modification has finished (no associated keycode)
481 | for (let eventName of ["keydown", "keypress", "keyup", "input", "change"]) {
482 | el.dispatchEvent(new Event(eventName, { bubbles: true }));
483 | }
484 |
485 | // re-set value if unchanged after firing post-fill events
486 | // (in case of sabotage by the site's own event handlers)
487 | if ((el.value || el.getAttribute("value")) === initialValue) {
488 | el.setAttribute("value", value);
489 | el.value = value;
490 | }
491 |
492 | // Finally unfocus the element
493 | el.dispatchEvent(new Event("blur", { bubbles: true }));
494 | return true;
495 | }
496 |
497 | // set window object
498 | window.browserpass = {
499 | fillLogin: fillLogin,
500 | focusOrSubmit: focusOrSubmit,
501 | };
502 | })();
503 |
--------------------------------------------------------------------------------
/src/manifest-chromium.json:
--------------------------------------------------------------------------------
1 | {
2 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvlVUvevBvdeIFpvK5Xjcbd/cV8AsMNLg0Y7BmUetSTagjts949Tp12mNmWmIEEaE9Zwmfjl1ownWiclGhsoPSf6x7nP/i0j8yROv6TYibXLhZet9y4vnUMgtCIkb3O5RnuOl0Y+V3XUADwxotmgT1laPUThymJoYnWPv+lwDkYiEopX2Aq2amzRj8aMogNBUbAIkCMxfa9WK3Vm0QTAUdV4ii9WqzbgjHVruQpiFVq99W2U9ddsWNZjOG/36sFREuHw+reulQgblp9FZdaN1Q9X5cGcT5bncQIRB6K3wZYa805gFENc93Wslmzu6aUSEKqqPymlI5ikedaPlXPmlqwIDAQAB",
3 | "manifest_version": 3,
4 | "name": "Browserpass",
5 | "description": "Browser extension for zx2c4's pass (password manager)",
6 | "version": "3.10.2",
7 | "author": "Maxim Baz , Steve Gilberd ",
8 | "homepage_url": "https://github.com/browserpass/browserpass-extension",
9 | "background": {
10 | "service_worker": "js/background.dist.js"
11 | },
12 | "icons": {
13 | "16": "icon16.png",
14 | "128": "icon.png"
15 | },
16 | "action": {
17 | "default_icon": {
18 | "16": "icon16.png",
19 | "128": "icon.png"
20 | },
21 | "default_popup": "popup/popup.html"
22 | },
23 | "options_ui": {
24 | "page": "options/options.html",
25 | "open_in_tab": false
26 | },
27 | "permissions": [
28 | "activeTab",
29 | "alarms",
30 | "tabs",
31 | "clipboardRead",
32 | "clipboardWrite",
33 | "nativeMessaging",
34 | "notifications",
35 | "offscreen",
36 | "scripting",
37 | "storage",
38 | "webRequest",
39 | "webRequestAuthProvider"
40 | ],
41 | "host_permissions": ["http://*/*", "https://*/*"],
42 | "content_security_policy": {
43 | "extension_pages": "default-src 'none'; font-src 'self'; img-src 'self' data:; script-src 'self'"
44 | },
45 | "commands": {
46 | "_execute_action": {
47 | "suggested_key": {
48 | "default": "Ctrl+Shift+L"
49 | }
50 | },
51 | "fillBest": {
52 | "suggested_key": {
53 | "default": "Ctrl+Shift+F"
54 | },
55 | "description": "Fill form with the best matching credentials"
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/manifest-firefox.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Browserpass",
4 | "description": "Browser extension for zx2c4's pass (password manager)",
5 | "version": "3.10.2",
6 | "author": "Maxim Baz , Steve Gilberd ",
7 | "homepage_url": "https://github.com/browserpass/browserpass-extension",
8 | "background": {
9 | "scripts": ["js/background.dist.js"]
10 | },
11 | "icons": {
12 | "16": "icon16.png",
13 | "128": "icon.png"
14 | },
15 | "action": {
16 | "default_icon": {
17 | "16": "icon16.png",
18 | "128": "icon.svg"
19 | },
20 | "default_popup": "popup/popup.html"
21 | },
22 | "options_ui": {
23 | "page": "options/options.html",
24 | "open_in_tab": false
25 | },
26 | "permissions": [
27 | "activeTab",
28 | "alarms",
29 | "tabs",
30 | "clipboardRead",
31 | "clipboardWrite",
32 | "nativeMessaging",
33 | "notifications",
34 | "scripting",
35 | "storage",
36 | "webRequest",
37 | "webRequestAuthProvider"
38 | ],
39 | "host_permissions": ["http://*/*", "https://*/*"],
40 | "content_security_policy": {
41 | "extension_pages": "default-src 'none'; font-src 'self'; img-src 'self' data:; script-src 'self'"
42 | },
43 | "browser_specific_settings": {
44 | "gecko": {
45 | "id": "browserpass@maximbaz.com",
46 | "strict_min_version": "58.0"
47 | }
48 | },
49 | "commands": {
50 | "_execute_action": {
51 | "suggested_key": {
52 | "default": "Ctrl+Shift+L"
53 | }
54 | },
55 | "fillBest": {
56 | "suggested_key": {
57 | "default": "Ctrl+Shift+F"
58 | },
59 | "description": "Fill form with the best matching credentials"
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/offscreen/offscreen.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/offscreen/offscreen.js:
--------------------------------------------------------------------------------
1 | //------------------------------------- Initialization --------------------------------------//
2 | "use strict";
3 | const clipboard = require("../helpers/clipboard");
4 |
5 | //----------------------------------- Function definitions ----------------------------------//
6 | chrome.runtime.onMessage.addListener(handleMessage);
7 |
8 | async function handleMessage(message, sender, sendResponse) {
9 | if (sender.id !== chrome.runtime.id) {
10 | // silently exit without responding when the source is foreign
11 | return;
12 | }
13 |
14 | // Return early if this message isn't meant for the offscreen document.
15 | if (message.target !== "offscreen-doc") {
16 | return;
17 | }
18 |
19 | // Dispatch the message to an appropriate handler.
20 | let reply;
21 | try {
22 | switch (message.type) {
23 | case "copy-data-to-clipboard":
24 | clipboard.writeToClipboard(message.data);
25 | break;
26 | case "read-from-clipboard":
27 | reply = clipboard.readFromClipboard();
28 | break;
29 | default:
30 | console.warn(`Unexpected message type received: '${message.type}'.`);
31 | }
32 | sendResponse({ status: "ok", message: reply || undefined });
33 | } catch (e) {
34 | sendResponse({ status: "error", message: e.toString() });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/options/interface.js:
--------------------------------------------------------------------------------
1 | module.exports = Interface;
2 |
3 | const m = require("mithril");
4 |
5 | /**
6 | * Options main interface
7 | *
8 | * @since 3.0.0
9 | *
10 | * @param object settings Settings object
11 | * @param function saveSettings Function to save settings
12 | * @param function clearUsageData Function to clear usage data
13 | * @return void
14 | */
15 | function Interface(settings, saveSettings, clearUsageData) {
16 | // public methods
17 | this.attach = attach;
18 | this.view = view;
19 |
20 | // fields
21 | this.settings = settings;
22 | this.saveSettings = saveSettings;
23 | this.saveEnabled = false;
24 | this.clearUsageData = clearUsageData;
25 | }
26 |
27 | /**
28 | * Attach the interface on the given element
29 | *
30 | * @since 3.0.0
31 | *
32 | * @param DOMElement element Target element
33 | * @return void
34 | */
35 | function attach(element) {
36 | m.mount(element, this);
37 | }
38 |
39 | /**
40 | * Generates vnodes for render
41 | *
42 | * @since 3.0.0
43 | *
44 | * @param function ctl Controller
45 | * @param object params Runtime params
46 | * @return []Vnode
47 | */
48 | function view(ctl, params) {
49 | var nodes = [];
50 | nodes.push(m("h3", "Basic settings"));
51 | nodes.push(
52 | createCheckbox.call(
53 | this,
54 | "autoSubmit",
55 | "Automatically submit forms after filling (not recommended)"
56 | )
57 | );
58 | nodes.push(
59 | createCheckbox.call(this, "enableOTP", "Enable support for OTP tokens (not recommended)")
60 | );
61 | nodes.push(createCheckbox.call(this, "hideBadge", "Hide badge counter on the toolbar icon"));
62 | nodes.push(createInput.call(this, "username", "Default username", "john.smith"));
63 | nodes.push(createInput.call(this, "gpgPath", "Custom gpg binary", "/path/to/gpg"));
64 |
65 | nodes.push(m("h3", "Theme"));
66 | nodes.push(
67 | createDropdown.call(this, "theme", [
68 | m("option", { value: "auto" }, "Auto"),
69 | m("option", { value: "dark" }, "Dark"),
70 | m("option", { value: "light" }, "Light"),
71 | ])
72 | );
73 |
74 | nodes.push(m("h3", "Custom store locations"));
75 | nodes.push(
76 | m("div", { class: "notice" }, "(this overrides default store and $PASSWORD_STORE_DIR)")
77 | );
78 | for (var storeId in this.settings.stores) {
79 | nodes.push(createCustomStore.call(this, storeId));
80 | }
81 | nodes.push(
82 | m(
83 | "a.add-store",
84 | {
85 | onclick: () => {
86 | addEmptyStore(this.settings.stores);
87 | this.saveEnabled = true;
88 | },
89 | },
90 | "Add store"
91 | )
92 | );
93 |
94 | if (typeof this.error !== "undefined") {
95 | nodes.push(m("div.error", this.error.message));
96 | }
97 | if (this.settings.hasOwnProperty("hostError")) {
98 | let hostError = this.settings.hostError;
99 | nodes.push(m("div.error", hostError.params.message));
100 | }
101 |
102 | nodes.push(
103 | m(
104 | "button.save",
105 | {
106 | disabled: !this.saveEnabled,
107 | onclick: async () => {
108 | try {
109 | this.settings = await this.saveSettings(this.settings);
110 | this.error = undefined;
111 | } catch (e) {
112 | this.error = e;
113 | }
114 | this.saveEnabled = false;
115 | m.redraw();
116 | },
117 | },
118 | "Save"
119 | )
120 | );
121 |
122 | nodes.push(
123 | m(
124 | "button.clearUsageData",
125 | {
126 | onclick: async () => {
127 | try {
128 | await this.clearUsageData();
129 | this.error = undefined;
130 | } catch (e) {
131 | this.error = e;
132 | }
133 | m.redraw();
134 | },
135 | },
136 | "Clear usage data"
137 | )
138 | );
139 | return nodes;
140 | }
141 |
142 | /**
143 | * Generates vnode for a input setting
144 | *
145 | * @since 3.0.0
146 | *
147 | * @param string key Settings key
148 | * @param string title Settings title
149 | * @param string placeholder Settings placeholder
150 | * @return Vnode
151 | */
152 | function createInput(key, title, placeholder) {
153 | return m("div.option", { class: key }, [
154 | m("label", [
155 | title,
156 | m("input[type=text]", {
157 | value: this.settings[key],
158 | placeholder: placeholder,
159 | onchange: (e) => {
160 | this.settings[key] = e.target.value;
161 | this.saveEnabled = true;
162 | },
163 | }),
164 | ]),
165 | ]);
166 | }
167 |
168 | /**
169 | * Generates vnode for a dropdown setting
170 | *
171 | * @since 3.3.1
172 | *
173 | * @param string key Settings key
174 | * @param array options Array of objects with value and text fields
175 | * @return Vnode
176 | */
177 | function createDropdown(key, options) {
178 | return m(
179 | "select",
180 | {
181 | value: this.settings[key],
182 | onchange: (e) => {
183 | this.settings[key] = e.target.value;
184 | this.saveEnabled = true;
185 | },
186 | },
187 | options
188 | );
189 | }
190 |
191 | /**
192 | * Generates vnode for a checkbox setting
193 | *
194 | * @since 3.0.0
195 | *
196 | * @param string key Settings key
197 | * @param string title Label for the checkbox
198 | * @return Vnode
199 | */
200 | function createCheckbox(key, title) {
201 | return m("div.option", { class: key }, [
202 | m("label", [
203 | m("input[type=checkbox]", {
204 | title: title,
205 | checked: this.settings[key],
206 | onchange: (e) => {
207 | this.settings[key] = e.target.checked;
208 | this.saveEnabled = true;
209 | },
210 | }),
211 | title,
212 | ]),
213 | ]);
214 | }
215 |
216 | /**
217 | * Generates vnode for a custom store configuration
218 | *
219 | * @since 3.0.0
220 | *
221 | * @param string storeId Store ID
222 | * @return Vnode
223 | */
224 | function createCustomStore(storeId) {
225 | let store = this.settings.stores[storeId];
226 |
227 | return m("div.option.custom-store", { class: "store-" + store.name }, [
228 | m("input[type=text].name", {
229 | title: "The name for this password store",
230 | value: store.name,
231 | placeholder: "name",
232 | onchange: (e) => {
233 | store.name = e.target.value;
234 | this.saveEnabled = true;
235 | },
236 | }),
237 | m("input[type=text].path", {
238 | title: "The full path to this password store",
239 | value: store.path,
240 | placeholder: "/path/to/store",
241 | onchange: (e) => {
242 | store.path = e.target.value;
243 | this.saveEnabled = true;
244 | },
245 | }),
246 | m("input[type=text].bgColor", {
247 | title: "Badge background color",
248 | value: store.bgColor,
249 | placeholder: "#626262",
250 | onchange: (e) => {
251 | store.bgColor = e.target.value;
252 | this.saveEnabled = true;
253 | },
254 | }),
255 | m("input[type=text].color", {
256 | title: "Badge text color",
257 | value: store.color,
258 | placeholder: "#c4c4c4",
259 | onchange: (e) => {
260 | store.color = e.target.value;
261 | this.saveEnabled = true;
262 | },
263 | }),
264 | m(
265 | "a.remove",
266 | {
267 | title: "Remove this password store",
268 | onclick: () => {
269 | delete this.settings.stores[storeId];
270 | this.saveEnabled = true;
271 | },
272 | },
273 | "[X]"
274 | ),
275 | ]);
276 | }
277 |
278 | /**
279 | * Generates new store ID
280 | *
281 | * @since 3.0.0
282 | *
283 | * @return string new store ID
284 | */
285 | function newId() {
286 | return Math.random().toString(36).substr(2, 9);
287 | }
288 |
289 | /**
290 | * Generates a new empty store
291 | *
292 | * @since 3.0.0
293 | *
294 | * @param []object stores List of stores to add a new store to
295 | * @return void
296 | */
297 | function addEmptyStore(stores) {
298 | let store = { id: newId(), name: "", path: "" };
299 | stores[store.id] = store;
300 | }
301 |
--------------------------------------------------------------------------------
/src/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |