├── .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 |
Loading options...
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | //------------------------------------- Initialisation --------------------------------------// 2 | "use strict"; 3 | 4 | require("chrome-extension-async"); 5 | const Interface = require("./interface"); 6 | 7 | run(); 8 | 9 | //----------------------------------- Function definitions ----------------------------------// 10 | 11 | /** 12 | * Handle an error 13 | * 14 | * @since 3.0.0 15 | * 16 | * @param Error error Error object 17 | * @param string type Error type 18 | */ 19 | function handleError(error, type = "error") { 20 | if (type == "error") { 21 | console.log(error); 22 | } 23 | var errorNode = document.createElement("div"); 24 | errorNode.setAttribute("class", "part " + type); 25 | errorNode.textContent = error.toString(); 26 | document.body.innerHTML = ""; 27 | document.body.appendChild(errorNode); 28 | } 29 | 30 | /** 31 | * Get settings 32 | * 33 | * @since 3.0.9 34 | * 35 | * @return object Settings object 36 | */ 37 | async function getSettings() { 38 | var response = await chrome.runtime.sendMessage({ action: "getSettings" }); 39 | if (response.status != "ok") { 40 | throw new Error(response.message); 41 | } 42 | 43 | // 'default' store must not be displayed or later attempted to be saved 44 | delete response.settings.stores.default; 45 | 46 | return response.settings; 47 | } 48 | 49 | /** 50 | * Save settings 51 | * 52 | * @since 3.0.0 53 | * 54 | * @param object settings Settings object 55 | * @return object Settings object 56 | */ 57 | async function saveSettings(settings) { 58 | var response = await chrome.runtime.sendMessage({ 59 | action: "saveSettings", 60 | settings: settings, 61 | }); 62 | if (response.status != "ok") { 63 | throw new Error(response.message); 64 | } 65 | 66 | // reload settings 67 | return await getSettings(); 68 | } 69 | 70 | /** 71 | * Clear usage data 72 | * 73 | * @since 3.0.10 74 | * 75 | * @return void 76 | */ 77 | async function clearUsageData() { 78 | var response = await chrome.runtime.sendMessage({ action: "clearUsageData" }); 79 | if (response.status != "ok") { 80 | throw new Error(response.message); 81 | } 82 | } 83 | 84 | /** 85 | * Run the main options logic 86 | * 87 | * @since 3.0.0 88 | * 89 | * @return void 90 | */ 91 | async function run() { 92 | try { 93 | var options = new Interface(await getSettings(), saveSettings, clearUsageData); 94 | options.attach(document.body); 95 | } catch (e) { 96 | handleError(e); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/options/options.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Open Sans"; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local("Open Sans"), url("/fonts/OpenSans-Regular.ttf") format("truetype"); 6 | } 7 | 8 | @font-face { 9 | font-family: "Open Sans"; 10 | font-style: normal; 11 | font-weight: 300; 12 | src: local("Open Sans Light"), url("/fonts/OpenSans-Light.ttf") format("truetype"); 13 | } 14 | 15 | html, 16 | body { 17 | box-sizing: border-box; 18 | overflow: scroll; 19 | margin: 0; 20 | } 21 | 22 | body { 23 | margin: 10px 20px 20px; 24 | } 25 | 26 | h3 { 27 | margin-top: 25px; 28 | } 29 | 30 | h3:first-child { 31 | margin-top: 0; 32 | } 33 | 34 | .notice { 35 | margin-top: -10px; 36 | margin-bottom: 10px; 37 | color: gray; 38 | font-size: 10px; 39 | } 40 | 41 | .option { 42 | display: flex; 43 | align-items: center; 44 | height: 16px; 45 | line-height: 16px; 46 | margin-bottom: 5px; 47 | margin-top: 5px; 48 | } 49 | 50 | .option input[type="checkbox"] { 51 | height: 12px; 52 | margin: 2px 6px 2px 0; 53 | padding: 0; 54 | } 55 | 56 | .option input[type="text"] { 57 | background-color: white; 58 | color: black; 59 | border: none; 60 | border-bottom: 1px solid #aaa; 61 | border-radius: 0; 62 | min-height: 0px; 63 | height: 21px; 64 | overflow: hidden; 65 | padding: 0; 66 | width: 125px; 67 | margin-left: 5px; 68 | } 69 | 70 | .option.custom-store input[type="text"] { 71 | margin: -4px 6px 0 0; 72 | width: 16%; 73 | } 74 | 75 | .option.custom-store input[type="text"].path { 76 | width: calc(100% - 16% * 3 - 6px * 3 - 16px); 77 | } 78 | 79 | .option.custom-store a.remove { 80 | color: #f00; 81 | display: block; 82 | height: 16px; 83 | line-height: 16px; 84 | margin: 0; 85 | padding: 0; 86 | text-decoration: none; 87 | width: 16px; 88 | cursor: pointer; 89 | } 90 | 91 | .add-store { 92 | cursor: pointer; 93 | display: block; 94 | margin-top: 12px; 95 | margin-bottom: 30px; 96 | color: rgb(17, 85, 204); 97 | text-decoration: underline; 98 | } 99 | 100 | .error { 101 | margin-bottom: 30px; 102 | color: red; 103 | } 104 | 105 | .save { 106 | cursor: pointer; 107 | margin-right: 10px; 108 | } 109 | 110 | @-moz-document url-prefix() { 111 | body { 112 | background: #fff; 113 | border: 1px solid #000; 114 | font-family: "Open Sans"; 115 | margin: 2px; 116 | padding: 12px; 117 | } 118 | 119 | .option.custom-store input[type="text"] { 120 | background: #fff; 121 | margin: 2px; 122 | } 123 | 124 | .option.custom-store a.remove { 125 | font-size: 12px; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserpass-extension", 3 | "version": "3.10.2", 4 | "description": "Browser extension for zx2c4's pass (password manager) - Community Edition.", 5 | "homepage": "https://github.com/browserpass/browserpass-extension", 6 | "license": "ISC", 7 | "author": [ 8 | { 9 | "name": "Maxim Baz", 10 | "email": "browserpass@maximbaz.com" 11 | }, 12 | { 13 | "name": "Steve Gilberd", 14 | "email": "steve@erayd.net" 15 | } 16 | ], 17 | "dependencies": { 18 | "@browserpass/url": "^1.1.6", 19 | "chrome-extension-async": "^3.4.1", 20 | "fuzzysort": "^1.1.4", 21 | "hash.js": "^1.1.7", 22 | "idb": "^4.0.5", 23 | "ignore": "^5.1.8", 24 | "mithril": "^1.1.7", 25 | "moment": "^2.30.1", 26 | "otplib": "^11.0.0", 27 | "sha1": "^1.1.1" 28 | }, 29 | "devDependencies": { 30 | "browserify": "^16.5.2", 31 | "less": "^3.12.2", 32 | "prettier": "^2.0.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/popup/colors-dark.less: -------------------------------------------------------------------------------- 1 | @import "colors.less"; 2 | 3 | .colors-dark { 4 | .colors( 5 | @bg-color: #414141, 6 | @default-bg-color: #393939, 7 | @hover-bg-color: #363636, 8 | @text-color: #c4c4c4, 9 | @error-text-color: #f00, 10 | @dim-text-color: #a0a0a0, 11 | @badge-color: #626262, 12 | @input-bg-color: #4a4a4a, 13 | @active-input-bg-color: #4f4f4f, 14 | @input-text-color: #eee, 15 | @hint-bg-color: #d79921, 16 | @hint-color: #363636, 17 | @match-text-bg-color: transparent, 18 | @match-text-color: #d79921, 19 | @invert-text-color: #414141, 20 | @snack-color: #525252, 21 | @snack-label-color: #afafaf, 22 | @progress-color: #bd861a, 23 | @edit-bg-color: #4a4a4a, 24 | 25 | // tree browser colors 26 | @treeopt-bg-color: #2e2e2e, 27 | 28 | // notifications 29 | @ntfy-hover-shadow: rgba(245, 245, 245, 0.7), 30 | @ntfy-info-color: #aaf3ff, 31 | @ntfy-info-bgcolor: #0c525e, 32 | @ntfy-info-border: #bee5eb, 33 | @ntfy-warning-color: #fff3d1, 34 | @ntfy-warning-bgcolor: #684e03, 35 | @ntfy-warning-border: #ffeeba, 36 | @ntfy-error-color: #ffd6d9, 37 | @ntfy-error-bgcolor: #540000, 38 | @ntfy-error-border: #f5c6cb, 39 | @ntfy-success-color: #dcffe4, 40 | @ntfy-success-bgcolor: #186029, 41 | @ntfy-success-border: #c3e6cb 42 | ); 43 | 44 | .details { 45 | .loading { 46 | background: url(/popup/page-loader-dark.gif) center no-repeat; 47 | background-size: 150px; 48 | background-position-y: 50px; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/popup/colors-light.less: -------------------------------------------------------------------------------- 1 | @import "colors.less"; 2 | 3 | .colors-light { 4 | .colors( 5 | @bg-color: #f1f3f5, 6 | @default-bg-color: #d8dee3, 7 | @hover-bg-color: #ced4da, 8 | @text-color: #343a40, 9 | @error-text-color: #e03131, 10 | @dim-text-color: #868e96, 11 | @badge-color: #b8b8b8, 12 | @input-bg-color: #eaeef1, 13 | @active-input-bg-color: #e8ebee, 14 | @input-text-color: #343a40, 15 | @hint-bg-color: #1c7ed6, 16 | @hint-color: #e7f5ff, 17 | @match-text-bg-color: #cfecff, 18 | @match-text-color: #1873ea, 19 | @invert-text-color: #f1f3f5, 20 | @snack-color: #7a7a7a, 21 | @snack-label-color: #7a7a7a, 22 | @progress-color: #c7d5ff, 23 | @edit-bg-color: #ffffff, 24 | 25 | // tree browser colors 26 | @treeopt-bg-color: #464545, 27 | 28 | // notifications 29 | @ntfy-hover-shadow: rgba(0, 0, 0, 0.3), 30 | @ntfy-info-color: #0c525e, 31 | @ntfy-info-bgcolor: #d1ecf1, 32 | @ntfy-info-border: #bee5eb, 33 | @ntfy-warning-color: #684e03, 34 | @ntfy-warning-bgcolor: #fff3cd, 35 | @ntfy-warning-border: #ffeeba, 36 | @ntfy-error-color: #721c24, 37 | @ntfy-error-bgcolor: #f8d7da, 38 | @ntfy-error-border: #f5c6cb, 39 | @ntfy-success-color: #155624, 40 | @ntfy-success-bgcolor: #d4edda, 41 | @ntfy-success-border: #c3e6cb 42 | ); 43 | 44 | .part.login .name .line1 .recent, 45 | .part.login .action.copy-password, 46 | .part.login .action.copy-user, 47 | .part.login .action.details, 48 | .part.login .action.edit, 49 | .part.login .action.save, 50 | .part.login .action.delete, 51 | .part.details .action.copy, 52 | .title .btn.back, 53 | .title .btn.save, 54 | .title .btn.edit { 55 | filter: invert(85%); 56 | } 57 | 58 | .part.login .name .line1 .recent:focus, 59 | .part.login .name .line1 .recent:hover, 60 | .part.login .action.copy-password:focus, 61 | .part.login .action.copy-password:hover, 62 | .part.login .action.copy-user:focus, 63 | .part.login .action.copy-user:hover, 64 | .part.login .action.details:focus, 65 | .part.login .action.details:hover, 66 | .title .btn.back:focus, 67 | .title .btn.back:hover, 68 | .title .btn.save:focus, 69 | .title .btn.save:hover, 70 | .title .btn.edit:focus, 71 | .title .btn.edit:hover { 72 | // colour such that invert(85%) ~= @hover-bg-color 73 | background-color: #0c0804; 74 | } 75 | 76 | .part.details .part.snack { 77 | &.line-otp { 78 | background: transparent; 79 | } 80 | .progress-container { 81 | background-color: #ffffff; 82 | z-index: -1; 83 | margin-top: -4px; 84 | height: 34px; 85 | } 86 | } 87 | 88 | .details { 89 | .loading { 90 | background: url(/popup/page-loader.gif) center no-repeat; 91 | background-size: 150px; 92 | background-position-y: 50px; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/popup/colors.less: -------------------------------------------------------------------------------- 1 | .colors( 2 | @bg-color, 3 | @default-bg-color, 4 | @hover-bg-color, 5 | @text-color, 6 | @error-text-color, 7 | @dim-text-color, 8 | @badge-color, 9 | @input-bg-color, 10 | @active-input-bg-color, 11 | @input-text-color, 12 | @hint-bg-color, 13 | @hint-color, 14 | @match-text-bg-color, 15 | @match-text-color, 16 | @invert-text-color, 17 | @snack-color, 18 | @snack-label-color, 19 | @progress-color, 20 | @edit-bg-color, 21 | // tree browser colors 22 | @treeopt-bg-color, 23 | // notification colors 24 | @ntfy-hover-shadow, 25 | @ntfy-info-color, 26 | @ntfy-info-bgcolor, 27 | @ntfy-info-border, 28 | @ntfy-warning-color, 29 | @ntfy-warning-bgcolor, 30 | @ntfy-warning-border, 31 | @ntfy-error-color, 32 | @ntfy-error-bgcolor, 33 | @ntfy-error-border, 34 | @ntfy-success-color, 35 | @ntfy-success-bgcolor, 36 | @ntfy-success-border) { 37 | html, 38 | body { 39 | background-color: @bg-color; 40 | color: @text-color; 41 | } 42 | 43 | .badge { 44 | background-color: @badge-color; 45 | } 46 | 47 | .addEdit, 48 | .details { 49 | .btn { 50 | &.generate { 51 | background-color: @edit-bg-color; 52 | } 53 | 54 | &:hover, 55 | &:focus { 56 | background-color: @hover-bg-color; 57 | } 58 | } 59 | 60 | .contents { 61 | .password { 62 | background-color: @edit-bg-color; 63 | } 64 | 65 | .password, 66 | .details { 67 | border-color: @snack-color; 68 | } 69 | } 70 | 71 | .location { 72 | select { 73 | background-color: @input-bg-color; 74 | color: @input-text-color; 75 | } 76 | 77 | .path { 78 | background-color: @edit-bg-color; 79 | border-color: @snack-color; 80 | color: @text-color; 81 | } 82 | } 83 | 84 | .actions { 85 | .save { 86 | background-color: @ntfy-success-bgcolor; 87 | border: 2px solid @ntfy-success-border; 88 | color: @ntfy-success-color; 89 | } 90 | .delete { 91 | background-color: @ntfy-error-bgcolor; 92 | border: 2px solid @ntfy-error-border; 93 | color: @ntfy-error-color; 94 | } 95 | .save, 96 | .delete { 97 | &:hover { 98 | box-shadow: 1px 1px 2px @ntfy-hover-shadow; 99 | } 100 | } 101 | } 102 | 103 | label { 104 | background-color: @snack-label-color; 105 | color: @invert-text-color; 106 | } 107 | 108 | input[type="number"], 109 | input[type="text"], 110 | textarea { 111 | background-color: @edit-bg-color; 112 | border-color: @snack-color; 113 | color: @text-color; 114 | } 115 | } 116 | 117 | .part.error { 118 | color: @error-text-color; 119 | } 120 | 121 | .part.add { 122 | background-color: @bg-color; 123 | &:hover, 124 | &:focus { 125 | outline: none; 126 | background-color: @hover-bg-color; 127 | } 128 | } 129 | 130 | .part.details { 131 | .part { 132 | &.snack { 133 | background-color: @edit-bg-color; 134 | border-color: @snack-color; 135 | .label { 136 | background-color: @snack-label-color; 137 | color: @invert-text-color; 138 | } 139 | .progress-container { 140 | background: transparent; 141 | .progress { 142 | background-color: @progress-color; 143 | } 144 | } 145 | } 146 | &.raw textarea { 147 | background-color: @edit-bg-color; 148 | border-color: @snack-color; 149 | color: @text-color; 150 | } 151 | } 152 | } 153 | 154 | .char.num, 155 | .char.punct { 156 | color: @match-text-color; 157 | } 158 | 159 | .part.search { 160 | background-color: @input-bg-color; 161 | } 162 | 163 | .part.search:focus-within { 164 | background-color: @active-input-bg-color; 165 | } 166 | 167 | .part.search > .hint { 168 | background-color: @hint-bg-color; 169 | color: @hint-color; 170 | } 171 | 172 | .part.search > input[type="text"] { 173 | background-color: transparent; 174 | color: @input-text-color; 175 | } 176 | 177 | .part.search > input[type="text"]::placeholder { 178 | color: @dim-text-color; 179 | } 180 | 181 | .logins:not(:hover):not(:focus-within) .part.login:first-child > .name { 182 | background-color: @default-bg-color; 183 | } 184 | 185 | .part.login:not(.details-header) > .name:hover, 186 | .part.login:not(.details-header) > .name:focus, 187 | .part.login:not(.details-header) > .action:hover, 188 | .part.login:not(.details-header) > .action:focus, 189 | .part.login:not(.details-header):focus > .name { 190 | background-color: @hover-bg-color; 191 | } 192 | 193 | .part.login em { 194 | background-color: @match-text-bg-color; 195 | color: @match-text-color; 196 | } 197 | 198 | .updates { 199 | border-top: 1px solid @hover-bg-color; 200 | 201 | span, 202 | a { 203 | color: @error-text-color; 204 | } 205 | } 206 | 207 | .m-notifications { 208 | .m-notification { 209 | &:hover { 210 | box-shadow: 1px 1px 2px @ntfy-hover-shadow; 211 | } 212 | 213 | &.info { 214 | color: @ntfy-info-color; 215 | background-color: @ntfy-info-bgcolor; 216 | border: 1px solid @ntfy-info-border; 217 | } 218 | 219 | &.warning { 220 | color: @ntfy-warning-color; 221 | background-color: @ntfy-warning-bgcolor; 222 | border: 1px solid @ntfy-warning-border; 223 | } 224 | 225 | &.error { 226 | color: @ntfy-error-color; 227 | background-color: @ntfy-error-bgcolor; 228 | border: 1px solid @ntfy-error-border; 229 | } 230 | 231 | &.success { 232 | color: @ntfy-success-color; 233 | background-color: @ntfy-success-bgcolor; 234 | border: 1px solid @ntfy-success-border; 235 | } 236 | } 237 | } 238 | 239 | div#tree-dirs { 240 | div.dropdown { 241 | a { 242 | background-color: @treeopt-bg-color; 243 | 244 | color: #fff; 245 | &:hover, 246 | &:focus-visible { 247 | filter: invert(15%); 248 | outline: none; 249 | } 250 | } 251 | } 252 | } 253 | 254 | dialog#browserpass-modal { 255 | color: @ntfy-info-color; 256 | background-color: @ntfy-info-bgcolor; 257 | border: 3px solid @ntfy-info-border; 258 | 259 | button { 260 | &:hover { 261 | box-shadow: 1px 1px 2px @ntfy-hover-shadow; 262 | } 263 | 264 | color: @ntfy-warning-color; 265 | background-color: @ntfy-warning-bgcolor; 266 | border: 1px solid @ntfy-warning-border; 267 | } 268 | 269 | &.warning { 270 | color: @ntfy-warning-color; 271 | background-color: @ntfy-warning-bgcolor; 272 | border: 1px solid @ntfy-warning-border; 273 | } 274 | &.error { 275 | color: @ntfy-error-color; 276 | background-color: @ntfy-error-bgcolor; 277 | border: 1px solid @ntfy-error-border; 278 | } 279 | 280 | &.warning, 281 | &.error { 282 | button { 283 | color: @ntfy-info-color; 284 | background-color: @ntfy-info-bgcolor; 285 | border: 1px solid @ntfy-info-border; 286 | } 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/popup/detailsInterface.js: -------------------------------------------------------------------------------- 1 | module.exports = DetailsInterface; 2 | 3 | const m = require("mithril"); 4 | const Moment = require("moment"); 5 | const helpers = require("../helpers/base"); 6 | const helpersUI = require("../helpers/ui"); 7 | const layout = require("./layoutInterface"); 8 | const Login = require("./models/Login"); 9 | const Settings = require("./models/Settings"); 10 | const notify = require("./notifications"); 11 | 12 | var persistSettingsModel = {}; 13 | 14 | /** 15 | * Login details interface 16 | * 17 | * @since 3.8.0 18 | * 19 | * @param object settings Settings model object 20 | * @return function View component 21 | */ 22 | function DetailsInterface(settingsModel) { 23 | persistSettingsModel = settingsModel; 24 | 25 | /** 26 | * DetailsView 27 | * 28 | * @since 3.8.0 29 | * 30 | * @param object vnode current vnode object 31 | */ 32 | return function (vnode) { 33 | // set default state 34 | var settings = {}, 35 | loading = true, 36 | loginObj = new Login(persistSettingsModel, { 37 | basename: "", 38 | dirname: "", 39 | store: {}, 40 | settings: settings, 41 | }), 42 | viewSettingsModel = persistSettingsModel; 43 | 44 | return { 45 | // public methods 46 | /** 47 | * Initialize component: get settings and login 48 | * 49 | * @since 3.8.0 50 | * 51 | * @param object vnode current vnode instance 52 | */ 53 | oninit: async function (vnode) { 54 | settings = await viewSettingsModel.get(); 55 | try { 56 | let tmpLogin = layout.getCurrentLogin(); 57 | if ( 58 | tmpLogin != null && 59 | Login.prototype.isLogin(tmpLogin) && 60 | tmpLogin.store.id == vnode.attrs.context.storeid && 61 | tmpLogin.login == vnode.attrs.context.login 62 | ) { 63 | // when returning from edit page 64 | loginObj = tmpLogin; 65 | } else { 66 | loginObj = await Login.prototype.get( 67 | settings, 68 | vnode.attrs.context.storeid, 69 | vnode.attrs.context.login 70 | ); 71 | } 72 | } catch (error) { 73 | console.log(error); 74 | notify.errorMsg(error.toString(), 0); 75 | m.route.set("/list"); 76 | } 77 | 78 | // get basename & dirname of entry 79 | loginObj.basename = loginObj.login.substr(loginObj.login.lastIndexOf("/") + 1); 80 | loginObj.dirname = loginObj.login.substr(0, loginObj.login.lastIndexOf("/")) + "/"; 81 | 82 | // trigger redraw after retrieving details 83 | layout.setCurrentLogin(loginObj); 84 | loading = false; 85 | m.redraw(); 86 | }, 87 | /** 88 | * Generates vnodes for render 89 | * 90 | * @since 3.6.0 91 | * 92 | * @param object vnode 93 | * @return []Vnode 94 | */ 95 | view: function (vnode) { 96 | const login = loginObj; 97 | const storeBgColor = Login.prototype.getStore(loginObj, "bgColor"); 98 | const storeColor = Login.prototype.getStore(loginObj, "color"); 99 | const secret = 100 | (loginObj.hasOwnProperty("fields") ? loginObj.fields.secret : null) || ""; 101 | const passChars = helpersUI.highlight(secret); 102 | 103 | var nodes = []; 104 | nodes.push( 105 | m("div.title", [ 106 | m("div.btn.back", { 107 | title: "Back to list", 108 | onclick: () => { 109 | m.route.set("/list"); 110 | }, 111 | }), 112 | m("span", "View credentials"), 113 | m("div.btn.edit", { 114 | title: `Edit ${login.basename}`, 115 | oncreate: m.route.link, 116 | href: `/edit/${loginObj.store.id}/${encodeURIComponent( 117 | loginObj.login 118 | )}`, 119 | }), 120 | ]), 121 | m("div.part.login.details-header", [ 122 | m("div.name", [ 123 | m("div.line1", [ 124 | m( 125 | "div.store.badge", 126 | { 127 | style: `background-color: ${storeBgColor}; 128 | color: ${storeColor}`, 129 | }, 130 | login.store.name 131 | ), 132 | m("div.path", [m.trust(login.dirname)]), 133 | login.recent.when > 0 134 | ? m("div.recent", { 135 | title: 136 | "Used here " + 137 | login.recent.count + 138 | " time" + 139 | (login.recent.count > 1 ? "s" : "") + 140 | ", last " + 141 | Moment(new Date(login.recent.when)).fromNow(), 142 | }) 143 | : null, 144 | ]), 145 | m("div.line2", [m.trust(login.basename)]), 146 | ]), 147 | ]), 148 | m("div.part.details", [ 149 | m("div.part.snack.line-secret", [ 150 | m("div.label", "Secret"), 151 | m("div.chars", passChars), 152 | m("div.action.copy", { 153 | title: "Copy Password", 154 | onclick: () => login.doAction("copyPassword"), 155 | }), 156 | ]), 157 | m("div.part.snack.line-login", [ 158 | m("div.label", "Login"), 159 | m("div", login.hasOwnProperty("fields") ? login.fields.login : ""), 160 | m("div.action.copy", { 161 | title: "Copy Username", 162 | onclick: () => login.doAction("copyUsername"), 163 | }), 164 | ]), 165 | (() => { 166 | if ( 167 | Settings.prototype.isSettings(settings) && 168 | Login.prototype.getStore(login, "enableOTP") && 169 | login.fields.otp && 170 | login.fields.otp.params.type === "totp" 171 | ) { 172 | // update progress 173 | // let progress = progress; 174 | let updateProgress = (vnode) => { 175 | let period = login.fields.otp.params.period; 176 | let remaining = period - ((Date.now() / 1000) % period); 177 | vnode.dom.style.transition = "none"; 178 | vnode.dom.style.width = `${(remaining / period) * 100}%`; 179 | setTimeout(function () { 180 | vnode.dom.style.transition = `width linear ${remaining}s`; 181 | vnode.dom.style.width = "0%"; 182 | }, 100); 183 | setTimeout(function () { 184 | m.redraw(); 185 | }, remaining); 186 | }; 187 | let progressNode = m("div.progress", { 188 | oncreate: updateProgress, 189 | onupdate: updateProgress, 190 | }); 191 | 192 | // display otp snack 193 | return m("div.part.snack.line-otp", [ 194 | m("div.label", "Token"), 195 | m("div.progress-container", progressNode), 196 | m("div", helpers.makeTOTP(login.fields.otp.params)), 197 | m("div.action.copy", { 198 | title: "Copy OTP", 199 | onclick: () => login.doAction("copyOTP"), 200 | }), 201 | ]); 202 | } 203 | })(), 204 | m( 205 | "div.part.raw", 206 | m( 207 | "textarea", 208 | { 209 | disabled: true, 210 | }, 211 | login.raw || "" 212 | ) 213 | ), 214 | ]) 215 | ); 216 | 217 | return m( 218 | "div.details", 219 | loading ? m(".loading", m("p", "Loading please wait ...")) : nodes 220 | ); 221 | }, 222 | }; 223 | }; 224 | } 225 | -------------------------------------------------------------------------------- /src/popup/icon-back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/popup/icon-bs-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/popup/icon-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/popup/icon-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/popup/icon-details.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/popup/icon-edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/popup/icon-generate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/popup/icon-history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/popup/icon-key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/popup/icon-save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 15 | 17 | 19 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/popup/icon-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /src/popup/icon-user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /src/popup/interface.js: -------------------------------------------------------------------------------- 1 | module.exports = Interface; 2 | 3 | const m = require("mithril"); 4 | const Moment = require("moment"); 5 | const SearchInterface = require("./searchinterface"); 6 | const BrowserpassURL = require("@browserpass/url"); 7 | const dialog = require("./modalDialog"); 8 | const helpers = require("../helpers/base"); 9 | const layout = require("./layoutInterface"); 10 | let overrideDefaultSearchOnce = true; 11 | 12 | /** 13 | * Popup main interface 14 | * 15 | * @since 3.0.0 16 | * 17 | * @param object settings Settings object 18 | * @param array logins Array of available logins 19 | * @return void 20 | */ 21 | function Interface(settings, logins) { 22 | // public methods 23 | this.attach = attach; 24 | this.view = view; 25 | this.renderMainView = renderMainView; 26 | this.search = search; 27 | 28 | // fields 29 | this.settings = settings; 30 | this.logins = logins; 31 | 32 | this.results = []; 33 | // check for chromium based browsers setting tab 34 | this.currentDomainOnly = !settings.tab.url.match(/^(chrome|brave|edge|opera|vivaldi|about):/); 35 | this.searchPart = new SearchInterface(this); 36 | 37 | // initialise with empty search 38 | this.search(""); 39 | } 40 | 41 | /** 42 | * Attach the interface on the given element 43 | * 44 | * @since 3.0.0 45 | * 46 | * @param DOMElement element Target element 47 | * @return void 48 | */ 49 | function attach(element) { 50 | m.mount(element, this); 51 | } 52 | 53 | /** 54 | * Generates vnodes for render 55 | * 56 | * @since 3.0.0 57 | * 58 | * @param function ctl Controller 59 | * @param object params Runtime params 60 | * @return []Vnode 61 | */ 62 | function view(ctl, params) { 63 | const nodes = []; 64 | // clear last viewed login 65 | layout.setCurrentLogin(null); 66 | 67 | nodes.push(...this.renderMainView(ctl, params)); 68 | 69 | if (this.settings.version < helpers.LATEST_NATIVE_APP_VERSION) { 70 | nodes.push( 71 | m("div.updates", [ 72 | m("span", "Update native host app: "), 73 | m( 74 | "a", 75 | { 76 | href: "https://github.com/browserpass/browserpass-native#installation", 77 | target: "_blank", 78 | }, 79 | "instructions" 80 | ), 81 | ]) 82 | ); 83 | } 84 | 85 | return nodes; 86 | } 87 | 88 | function renderMainView(ctl, params) { 89 | var nodes = []; 90 | nodes.push(m(this.searchPart)); 91 | 92 | nodes.push( 93 | m( 94 | "div.logins", 95 | this.results.map(function (result) { 96 | const storeBgColor = result.store.bgColor || result.store.settings.bgColor; 97 | const storeColor = result.store.color || result.store.settings.color; 98 | 99 | return m( 100 | "div.part.login", 101 | { 102 | onclick: function (e) { 103 | var action = e.target.getAttribute("action"); 104 | if (action) { 105 | result.doAction(action); 106 | } 107 | }, 108 | onkeydown: keyHandler.bind(result), 109 | tabindex: 0, 110 | }, 111 | [ 112 | m( 113 | "div.name", 114 | { 115 | key: result.index, 116 | tabindex: 0, 117 | title: "Fill username / password | ", 118 | onclick: function (e) { 119 | result.doAction("fill"); 120 | }, 121 | onkeydown: keyHandler.bind(result), 122 | }, 123 | [ 124 | m("div.line1", [ 125 | m( 126 | "div.store.badge", 127 | { 128 | style: `background-color: ${storeBgColor}; 129 | color: ${storeColor}`, 130 | }, 131 | result.store.name 132 | ), 133 | m("div.path", [m.trust(result.path)]), 134 | result.recent.when > 0 135 | ? m("div.recent", { 136 | title: 137 | "Used here " + 138 | result.recent.count + 139 | " time" + 140 | (result.recent.count > 1 ? "s" : "") + 141 | ", last " + 142 | Moment(new Date(result.recent.when)).fromNow(), 143 | }) 144 | : null, 145 | ]), 146 | m("div.line2", [m.trust(result.display)]), 147 | ] 148 | ), 149 | m("div.action.copy-user", { 150 | tabindex: 0, 151 | title: "Copy username | ", 152 | action: "copyUsername", 153 | }), 154 | m("div.action.copy-password", { 155 | tabindex: 0, 156 | title: "Copy password | ", 157 | action: "copyPassword", 158 | }), 159 | m("div.action.details", { 160 | tabindex: 0, 161 | title: "Open Details | ", 162 | oncreate: m.route.link, 163 | onupdate: m.route.link, 164 | href: `/details/${result.store.id}/${encodeURIComponent( 165 | result.loginPath 166 | )}`, 167 | }), 168 | ] 169 | ); 170 | }) 171 | ), 172 | m( 173 | "div.part.add", 174 | { 175 | tabindex: 0, 176 | title: "Add new login | ", 177 | oncreate: m.route.link, 178 | onupdate: m.route.link, 179 | href: `/add`, 180 | onkeydown: (e) => { 181 | e.preventDefault(); 182 | 183 | function goToElement(element) { 184 | element.focus(); 185 | element.scrollIntoView(); 186 | } 187 | 188 | let lastLogin = document.querySelector(".logins").lastChild; 189 | let searchInput = document.querySelector(".part.search input[type=text]"); 190 | switch (e.code) { 191 | case "Tab": 192 | if (e.shiftKey) { 193 | goToElement(lastLogin); 194 | } else { 195 | goToElement(searchInput); 196 | } 197 | break; 198 | case "Home": 199 | goToElement(searchInput); 200 | break; 201 | case "ArrowUp": 202 | goToElement(lastLogin); 203 | break; 204 | case "ArrowDown": 205 | goToElement(searchInput); 206 | break; 207 | case "Enter": 208 | e.target.click(); 209 | case "KeyA": 210 | if (e.ctrlKey) { 211 | e.target.click(); 212 | } 213 | break; 214 | default: 215 | break; 216 | } 217 | }, 218 | }, 219 | "Add credentials" 220 | ) 221 | ); 222 | 223 | return nodes; 224 | } 225 | 226 | /** 227 | * Run a search 228 | * 229 | * @param string searchQuery Search query 230 | * @return void 231 | */ 232 | function search(searchQuery) { 233 | const authUrl = overrideDefaultSearchOnce && helpers.parseAuthUrl(this.settings.tab.url); 234 | 235 | if (overrideDefaultSearchOnce && this.settings.authRequested && authUrl) { 236 | const authUrlInfo = new BrowserpassURL(authUrl); 237 | searchQuery = authUrlInfo.validDomain ? authUrlInfo.domain : authUrlInfo.hostname ?? ""; 238 | this.results = helpers.filterSortLogins(this.logins, searchQuery, true); 239 | } else { 240 | this.results = helpers.filterSortLogins(this.logins, searchQuery, this.currentDomainOnly); 241 | } 242 | overrideDefaultSearchOnce = false; 243 | } 244 | 245 | /** 246 | * Handle result key presses 247 | * 248 | * @param Event e Keydown event 249 | * @param object this Result object 250 | * @return void 251 | */ 252 | function keyHandler(e) { 253 | e.preventDefault(); 254 | var login = e.target.classList.contains("login") ? e.target : e.target.closest(".login"); 255 | 256 | switch (e.code) { 257 | case "Tab": 258 | var partElement = e.target.closest(".part"); 259 | var targetElement = e.shiftKey ? "previousElementSibling" : "nextElementSibling"; 260 | if (partElement[targetElement] && partElement[targetElement].hasAttribute("tabindex")) { 261 | partElement[targetElement].focus(); 262 | } else if (e.target == document.querySelector(".logins").lastChild) { 263 | document.querySelector(".part.add").focus(); 264 | } else { 265 | document.querySelector(".part.search input[type=text]").focus(); 266 | } 267 | break; 268 | case "ArrowDown": 269 | if (login.nextElementSibling) { 270 | login.nextElementSibling.focus(); 271 | } else { 272 | document.querySelector(".part.add").focus(); 273 | } 274 | break; 275 | case "ArrowUp": 276 | if (login.previousElementSibling) { 277 | login.previousElementSibling.focus(); 278 | } else { 279 | document.querySelector(".part.search input[type=text]").focus(); 280 | } 281 | break; 282 | case "ArrowRight": 283 | if (e.target.classList.contains("login")) { 284 | e.target.querySelector(".action").focus(); 285 | } else if (e.target.nextElementSibling) { 286 | e.target.nextElementSibling.focus(); 287 | } else { 288 | e.target.click(); 289 | } 290 | break; 291 | case "ArrowLeft": 292 | if (e.target.previousElementSibling.classList.contains("action")) { 293 | e.target.previousElementSibling.focus(); 294 | } else { 295 | login.focus(); 296 | } 297 | break; 298 | case "Enter": 299 | if (e.target.hasAttribute("action")) { 300 | this.doAction(e.target.getAttribute("action")); 301 | } else if (e.target.classList.contains("details")) { 302 | e.target.click(); 303 | } else { 304 | this.doAction("fill"); 305 | } 306 | break; 307 | case "KeyA": 308 | if (e.ctrlKey) { 309 | document.querySelector(".part.add").click(); 310 | } 311 | break; 312 | case "KeyC": 313 | if (e.ctrlKey) { 314 | if (e.shiftKey || document.activeElement.classList.contains("copy-user")) { 315 | this.doAction("copyUsername"); 316 | } else { 317 | this.doAction("copyPassword"); 318 | } 319 | } 320 | break; 321 | case "KeyG": 322 | if (e.ctrlKey) { 323 | const event = e; 324 | const target = this; 325 | dialog.open( 326 | helpers.LAUNCH_URL_DEPRECATION_MESSAGE, 327 | function () { 328 | target.doAction(event.shiftKey ? "launchInNewTab" : "launch"); 329 | }, 330 | false 331 | ); 332 | } 333 | break; 334 | case "KeyO": 335 | if (e.ctrlKey) { 336 | e.target.querySelector("div.action.details").click(); 337 | } 338 | break; 339 | case "Home": { 340 | document.querySelector(".part.search input[type=text]").focus(); 341 | document.querySelector(".logins").scrollTo(0, 0); 342 | window.scrollTo(0, 0); 343 | break; 344 | } 345 | case "End": { 346 | let logins = document.querySelectorAll(".login"); 347 | if (logins.length) { 348 | let target = logins.item(logins.length - 1); 349 | target.scrollIntoView(); 350 | } 351 | document.querySelector(".part.add").focus(); 352 | break; 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/popup/layoutInterface.js: -------------------------------------------------------------------------------- 1 | // libs 2 | const m = require("mithril"); 3 | // components 4 | const dialog = require("./modalDialog"); 5 | const Notifications = require("./notifications"); 6 | // models 7 | const Settings = require("./models/Settings"); 8 | const Tree = require("./models/Tree"); 9 | 10 | /** 11 | * Page / layout wrapper component. Used to share global 12 | * message and ui component functionality with any child 13 | * components. 14 | * 15 | * Also maintain a pseudo session state. 16 | */ 17 | 18 | let session = { 19 | // current decrypted login object 20 | current: null, 21 | // map of store key to array of login files 22 | logins: {}, 23 | // settings 24 | settings: null, 25 | // Tree instances with storeId as key 26 | trees: null, 27 | }; 28 | 29 | /** 30 | * Page layout component 31 | * @since 3.8.0 32 | */ 33 | let LayoutInterface = { 34 | oncreate: async function (vnode) { 35 | if ( 36 | Settings.prototype.isSettings(session.settings) && 37 | Settings.prototype.canTree(session.settings) 38 | ) { 39 | session.trees = await Tree.prototype.getAll(session.settings); 40 | } 41 | document.addEventListener("keydown", esacpeKeyHandler); 42 | }, 43 | view: function (vnode) { 44 | vnode.children.push(m(Notifications)); 45 | vnode.children.push(m(dialog)); 46 | 47 | return m(".layout", vnode.children); 48 | }, 49 | }; 50 | 51 | /** 52 | * Set login on details page after successful decrpytion 53 | * @since 3.8.0 54 | * 55 | * @param {object} login set session login object 56 | */ 57 | function setCurrentLogin(login) { 58 | session.current = login; 59 | } 60 | 61 | /** 62 | * Get current login on edit page to avoid 2nd decryption request 63 | * @since 3.8.0 64 | * 65 | * @returns {object} current login object 66 | */ 67 | function getCurrentLogin() { 68 | return session.current; 69 | } 70 | 71 | /** 72 | * Respond with boolean if combination of store id and login currently exist. 73 | * 74 | * @since 3.8.0 75 | * 76 | * @param {string} storeId unique store id 77 | * @param {string} login relative file path without file extension 78 | * @returns {boolean} 79 | */ 80 | function storeIncludesLogin(storeId, login) { 81 | if (!session.logins[storeId]) { 82 | return false; 83 | } 84 | 85 | if (!session.logins[storeId].length) { 86 | return false; 87 | } 88 | 89 | search = `${login.trim().trimStart("/")}.gpg`; 90 | return session.logins[storeId].includes(search); 91 | } 92 | 93 | /** 94 | * Set session object containing list of password files. 95 | * 96 | * @since 3.8.0 97 | * 98 | * @param {object} logins raw untouched object with store id 99 | * as keys each an array containing list of files for respective 100 | * password store. 101 | */ 102 | function setStoreLogins(logins = {}) { 103 | if (Object.prototype.isPrototypeOf(logins)) { 104 | session.logins = logins; 105 | } 106 | } 107 | 108 | /** 109 | * Store a single settings object for the current session 110 | * @since 3.8.0 111 | * 112 | * @param {object} settings settings object 113 | */ 114 | function setSessionSettings(settings) { 115 | if (Settings.prototype.isSettings(settings)) { 116 | session.settings = settings; 117 | } 118 | } 119 | 120 | /** 121 | * Get settings object for the current session 122 | * @since 3.8.0 123 | * 124 | * @returns {object} settings object 125 | */ 126 | function getSessionSettings() { 127 | return session.settings; 128 | } 129 | 130 | function getStoreTree(storeId) { 131 | return session.trees[storeId]; 132 | } 133 | 134 | /** 135 | * Handle all keydown events on the dom for the Escape key 136 | * 137 | * @since 3.8.0 138 | * 139 | * @param {object} e keydown event 140 | */ 141 | function esacpeKeyHandler(e) { 142 | switch (e.code) { 143 | case "Escape": 144 | // stop escape from closing pop up 145 | e.preventDefault(); 146 | let path = m.route.get(); 147 | 148 | if (path == "/add") { 149 | if (document.querySelector("#tree-dirs") == null) { 150 | // dir tree already hidden, go to previous page 151 | m.route.set("/list"); 152 | } else { 153 | // trigger click on an element other than input filename 154 | // which does not have a click handler, to close drop down 155 | document.querySelector(".store .storePath").click(); 156 | } 157 | } else if (path.startsWith("/details")) { 158 | m.route.set("/list"); 159 | } else if (path.startsWith("/edit")) { 160 | m.route.set(path.replace(/^\/edit/, "/details")); 161 | } 162 | break; 163 | } 164 | } 165 | 166 | module.exports = { 167 | LayoutInterface, 168 | getCurrentLogin, 169 | getSessionSettings, 170 | getStoreTree, 171 | setCurrentLogin, 172 | setStoreLogins, 173 | setSessionSettings, 174 | storeIncludesLogin, 175 | }; 176 | -------------------------------------------------------------------------------- /src/popup/modalDialog.js: -------------------------------------------------------------------------------- 1 | const m = require("mithril"); 2 | const redraw = require("../helpers/redraw"); 3 | 4 | const modalId = "browserpass-modal"; 5 | const CANCEL = "Cancel"; 6 | const CONFIRM = "Confirm"; 7 | 8 | /** 9 | * Basic mirthil dialog component. Shows modal dialog with 10 | * provided message content and passes back boolean 11 | * user response via a callback function. 12 | */ 13 | 14 | var callBackFn = null, 15 | cancelButtonText = null, 16 | confirmButtonText = null, 17 | modalElement = null, 18 | modalContent = null; 19 | 20 | /** 21 | * Handle modal button click. 22 | * 23 | * Trigger callback with boolean response, hide modal, clear values. 24 | * 25 | * @since 3.8.0 26 | * 27 | */ 28 | function buttonClick(response = false) { 29 | // run action handler 30 | if (typeof callBackFn === "function") { 31 | callBackFn(response); 32 | } 33 | 34 | // close and clear modal content state 35 | modalElement.close(); 36 | callBackFn = null; 37 | modalContent = null; 38 | } 39 | 40 | let Modal = { 41 | view: (node) => { 42 | return m("dialog", { id: modalId }, [ 43 | m(".modal-content", {}, m.trust(modalContent)), 44 | m(".modal-actions", {}, [ 45 | cancelButtonText 46 | ? m( 47 | "button.cancel", 48 | { 49 | onclick: () => { 50 | buttonClick(false); 51 | }, 52 | }, 53 | cancelButtonText 54 | ) 55 | : null, 56 | m( 57 | "button.confirm", 58 | { 59 | onclick: () => { 60 | buttonClick(true); 61 | }, 62 | }, 63 | confirmButtonText 64 | ), 65 | ]), 66 | ]); 67 | }, 68 | /** 69 | * Show dialog component after args validation 70 | * 71 | * @since 3.8.0 72 | * 73 | * @param {string} request object, with type, or string message (html) to render in main body of dialog 74 | * @param {function} callback function which accepts a single boolean argument 75 | * @param {string} cancelText text to display on the negative response button 76 | * @param {string} confirmText text to display on the positive response button 77 | */ 78 | open: ( 79 | request = "", 80 | callback = (resp = false) => {}, 81 | cancelText = CANCEL, 82 | confirmText = CONFIRM 83 | ) => { 84 | if (typeof callback !== "function") { 85 | return null; 86 | } 87 | 88 | let message = ""; 89 | let type = "info"; 90 | switch (typeof request) { 91 | case "string": 92 | if (!request.length) { 93 | return null; 94 | } 95 | message = request; 96 | break; 97 | case "object": 98 | if (typeof request?.message !== "string") { 99 | return null; 100 | } 101 | message = request.message; 102 | 103 | if (["info", "warning", "error"].includes(request?.type)) { 104 | type = request.type; 105 | } 106 | break; 107 | default: 108 | return null; 109 | } 110 | 111 | if (typeof cancelText == "string" && cancelText.length) { 112 | cancelButtonText = cancelText; 113 | } else if (cancelText === false) { 114 | cancelButtonText = undefined; 115 | } else { 116 | cancelButtonText = CANCEL; 117 | } 118 | 119 | if (typeof confirmText == "string" && confirmText.length) { 120 | confirmButtonText = confirmText; 121 | } else { 122 | confirmButtonText = CONFIRM; 123 | } 124 | 125 | modalElement = document.getElementById(modalId); 126 | modalElement.classList.remove(...modalElement.classList); 127 | modalElement.classList.add([type]); 128 | callBackFn = callback; 129 | modalContent = message; 130 | modalElement.showModal(); 131 | m.redraw(); 132 | 133 | redraw.increaseModalHeight(modalElement); 134 | }, 135 | }; 136 | 137 | module.exports = Modal; 138 | -------------------------------------------------------------------------------- /src/popup/models/Login.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("chrome-extension-async"); 4 | const sha1 = require("sha1"); 5 | const helpers = require("../../helpers/base"); 6 | const helpersUI = require("../../helpers/ui"); 7 | const Settings = require("./Settings"); 8 | 9 | // Search for one of the secret prefixes 10 | // from Array helpers.fieldsPrefix.secret 11 | const 12 | multiLineSecretRegEx = RegExp(`^(${helpers.fieldsPrefix.secret.join("|")}): `, 'mi') 13 | ; 14 | 15 | /** 16 | * Login Constructor() 17 | * 18 | * @since 3.8.0 19 | * 20 | * @param {object} settings 21 | * @param {object} login (optional) Extend an existing 22 | * login object to be backwards and forwards compatible. 23 | */ 24 | function Login(settings, login = {}) { 25 | if (Login.prototype.isLogin(login)) { 26 | // content sha used to determine if login has changes, see Login.prototype.isNew 27 | this.contentSha = sha1(login.login + sha1(login.raw || '')); 28 | } else { 29 | this.allowFill = true; 30 | this.fields = {}; 31 | this.host = null; 32 | this.login = ''; 33 | this.recent = { 34 | when: 0, 35 | count: 0, 36 | }; 37 | // a null content sha identifies this a new entry, see Login.prototype.isNew 38 | this.contentSha = null; 39 | } 40 | 41 | // Set object properties 42 | let setRaw = false; 43 | for (const prop in login) { 44 | this[prop] = login[prop]; 45 | if (prop === 'raw' && login[prop].length > 0) { 46 | // update secretPrefix after everything else 47 | setRaw = true; 48 | } 49 | } 50 | 51 | if (setRaw) { 52 | this.setRawDetails(login['raw']); 53 | } 54 | 55 | this.settings = settings; 56 | // This ensures doAction works in detailInterface, 57 | // and any other view in which it is necessary. 58 | this.doAction = helpersUI.withLogin.bind({ 59 | settings: settings, login: login 60 | }); 61 | } 62 | 63 | 64 | /** 65 | * Determines if the login object is new or not 66 | * 67 | * @since 3.8.0 68 | * 69 | * @returns {boolean} 70 | */ 71 | Login.prototype.isNew = function (login) { 72 | return login.hasOwnProperty("contentSha") && login.contentSha === null; 73 | } 74 | 75 | /** 76 | * Remove login entry 77 | * 78 | * @since 3.8.0 79 | * 80 | * @param {object} login Login entry to be deleted 81 | * @returns {object} Response or an empty object 82 | */ 83 | Login.prototype.delete = async function (login) { 84 | if (Login.prototype.isValid(login)) { 85 | const request = helpers.deepCopy(login); 86 | 87 | let response = await chrome.runtime.sendMessage({ 88 | action: "delete", login: request 89 | }); 90 | 91 | if (response.status != "ok") { 92 | throw new Error(response.message); 93 | } 94 | return response; 95 | } 96 | return {}; 97 | } 98 | 99 | /** 100 | * Generate a new password 101 | * 102 | * @since 3.8.0 103 | * 104 | * @param {int} length New secret length 105 | * @param {boolean} symbols Use symbols or not, default: false 106 | * @return string 107 | */ 108 | Login.prototype.generateSecret = function ( 109 | length = 16, 110 | useSymbols = false 111 | ) { 112 | let 113 | secret = "", 114 | value = new Uint8Array(1), 115 | alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", 116 | // double quote and backslash are at the end and escaped 117 | symbols = "!#$%&'()*+,-./:;<=>?@[]^_`{|}~.\"\\", 118 | options = "" 119 | ; 120 | 121 | options = (Boolean(useSymbols)) ? `${alphabet}${symbols}` : alphabet; 122 | 123 | while (secret.length < length) { 124 | crypto.getRandomValues(value); 125 | if (value[0] < options.length) { 126 | secret += options[value[0]]; 127 | } 128 | } 129 | return secret; 130 | } 131 | 132 | /** 133 | * Request a list of all login files and then 134 | * extend them with Login.prototype. 135 | * 136 | * @since 3.8.0 137 | * @throws {error} host response errors 138 | * 139 | * @param {object} settings Settings object 140 | * @returns {array} Logins 141 | */ 142 | Login.prototype.getAll = async function(settings) { 143 | // get list of logins 144 | let response = await chrome.runtime.sendMessage({ action: "listFiles" }); 145 | if (response.status != "ok") { 146 | throw new Error(response.message); 147 | } 148 | 149 | let logins = [] 150 | helpers.prepareLogins(response.files, settings).forEach(obj => { 151 | logins.push(new Login(settings, obj)); 152 | }); 153 | 154 | return { raw: response.files, processed: logins }; 155 | } 156 | 157 | /** 158 | * Request decrypted details of login from host for store id. 159 | * 160 | * @since 3.8.0 161 | * @throws {error} host response errors 162 | * 163 | * @param {object} settings Settings object 164 | * @param {string} storeid store id 165 | * @param {string} lpath relative file path, with extension, of login in store 166 | * @returns Login object 167 | */ 168 | Login.prototype.get = async function(settings, storeid, lpath) { 169 | let login = helpers.prepareLogin(settings, storeid, lpath); 170 | 171 | var response = await chrome.runtime.sendMessage({ 172 | action: "getDetails", login: login, params: {} 173 | }); 174 | 175 | if (response.status != "ok") { 176 | throw new Error(response.message); 177 | } 178 | 179 | return new Login(settings, response.login); 180 | } 181 | 182 | /** 183 | * Returns fields.secret or first line from fields.raw 184 | * 185 | * See also Login.prototype.getRawPassword and the 186 | * functions: setSecret(), setRawDetails() in src/popup/addEditInterface.js 187 | * 188 | * @since 3.8.0 189 | * 190 | * @returns {string} secret 191 | */ 192 | Login.prototype.getPassword = function() { 193 | if (typeof this.fields == 'object' && this.fields.hasOwnProperty("secret")) { 194 | return this.fields.secret; 195 | } 196 | return this.getRawPassword(); 197 | } 198 | 199 | /** 200 | * Return only password from fields.raw 201 | * 202 | * Is used with in combination with Login.prototype.getPassword and the 203 | * functions: setSecret(), setRawDetails() in src/popup/addEditInterface.js 204 | * 205 | * @since 3.8.0 206 | * 207 | * @returns {string} secret 208 | */ 209 | Login.prototype.getRawPassword = function() { 210 | if (typeof this.raw == 'string' && this.raw.length > 0) { 211 | const text = this.raw; 212 | 213 | return this.getSecretDetails(text).password; 214 | } 215 | return ""; 216 | } 217 | 218 | /** 219 | * Extract secret password and prefix from raw text string 220 | * Private 221 | * @param {string} text 222 | * @returns {object} 223 | */ 224 | function getSecretDetails(text = "") { 225 | let results = { 226 | prefix: null, 227 | password: "", 228 | } 229 | 230 | if (typeof text == 'string' && text.length > 0) { 231 | let index = text.search(multiLineSecretRegEx); 232 | 233 | // assume first line 234 | if (index == -1) { 235 | results.password = text.split(/[\n\r]+/, 1)[0].trim(); 236 | } else { 237 | const secret = text.substring(index).split(/[\n\r]+/, 1)[0].trim(); 238 | // only take first instance of "prefix: " 239 | index = secret.search(": "); 240 | results.prefix = secret.substring(0, index); 241 | results.password = secret.substring(index+2); 242 | } 243 | } 244 | 245 | return results; 246 | } 247 | 248 | /** 249 | * Retrieve store object. Can optionally return only sub path value. 250 | * 251 | * @since 3.8.0 252 | * 253 | * @param {object} login Login object 254 | * @param {string} property (optional) store sub property path value to return 255 | */ 256 | Login.prototype.getStore = function(login, property = "") { 257 | let 258 | settingsValue = Settings.prototype.getStore(login.settings, property), 259 | store = (login.hasOwnProperty("store")) ? login.store : {}, 260 | value = null 261 | ; 262 | 263 | switch (property) { 264 | case "color": 265 | case "bgColor": 266 | if (store.hasOwnProperty(property)) { 267 | value = store[property]; 268 | } 269 | break; 270 | 271 | default: 272 | if (property != "" && store.hasOwnProperty(property)) { 273 | value = store[property]; 274 | } 275 | break; 276 | } 277 | 278 | return value || settingsValue; 279 | } 280 | 281 | /** 282 | * Build style string for a login's store colors with which 283 | * to apply to an html element 284 | * 285 | * @since 3.8.0 286 | * 287 | * @param {object} login to pull store color settings from 288 | * @returns {string} 289 | */ 290 | Login.prototype.getStoreStyle = function (login) { 291 | if (!Login.prototype.isLogin(login)) { 292 | return ""; 293 | } 294 | const color = Login.prototype.getStore(login, "color"); 295 | const bgColor = Login.prototype.getStore(login, "bgColor"); 296 | 297 | return `color: ${color}; background-color: ${bgColor};` 298 | } 299 | 300 | /** 301 | * Determine if secretPrefix property has been set for 302 | * the current Login object: "this" 303 | * 304 | * @since 3.8.0 305 | * 306 | * @returns {boolean} 307 | */ 308 | Login.prototype.hasSecretPrefix = function () { 309 | let results = []; 310 | 311 | results.push(this.hasOwnProperty('secretPrefix')); 312 | results.push(Boolean(this.secretPrefix)); 313 | results.push(helpers.fieldsPrefix.secret.includes(this.secretPrefix)); 314 | 315 | return results.every(Boolean); 316 | } 317 | 318 | /** 319 | * Returns a boolean indication on if object passed 320 | * has the minimum required login propteries, 321 | * Login.prototype.isPrototypeOf(login) IS NOT the goal of this. 322 | * @since 3.8.0 323 | * 324 | * @param {object} login Login object 325 | * @returns Boolean 326 | */ 327 | Login.prototype.isLogin = function(login) { 328 | if (typeof login == 'undefined') { 329 | return false; 330 | } 331 | 332 | let results = []; 333 | 334 | results.push(login.hasOwnProperty('allowFill') && typeof login.allowFill == 'boolean'); 335 | results.push(login.hasOwnProperty('login') && typeof login.login == 'string'); 336 | results.push(login.hasOwnProperty('store') && typeof login.store == 'object'); 337 | results.push(login.hasOwnProperty('host')); 338 | results.push(login.hasOwnProperty('recent') && typeof login.recent == 'object'); 339 | 340 | return results.every(Boolean); 341 | } 342 | 343 | /** 344 | * Validation, determine if object passed is a 345 | * Login.prototype and is ready to be saved. 346 | * 347 | * @since 3.8.0 348 | * 349 | * @param {object} login Login object to validated 350 | */ 351 | Login.prototype.isValid = function(login) { 352 | let results = []; 353 | 354 | results.push(Login.prototype.isLogin(login)); 355 | results.push(Login.prototype.isPrototypeOf(login)); 356 | results.push(login.hasOwnProperty('login') && login.login.length > 0); 357 | results.push(login.hasOwnProperty('raw') && typeof login.raw == 'string' && login.raw.length > 0); 358 | 359 | return results.every(Boolean); 360 | } 361 | 362 | /** 363 | * Calls validation for Login and if it passes, 364 | * then calls chrome.runtime.sendMessage() 365 | * with {action: "add/save"} for new/existing secrets. 366 | * 367 | * @since 3.8.0 368 | * 369 | * @param {object} login Login object to be saved. 370 | * @returns {object} Response or an empty object. 371 | */ 372 | Login.prototype.save = async function(login) { 373 | if (Login.prototype.isValid(login)) { 374 | const request = helpers.deepCopy(login); 375 | const action = (this.isNew(login)) ? "add" : "save"; 376 | 377 | let response = await chrome.runtime.sendMessage({ 378 | action: action, login: request, params: { rawContents: request.raw } 379 | }); 380 | 381 | if (response.status != "ok") { 382 | throw new Error(response.message); 383 | } 384 | return response; 385 | } 386 | return {}; 387 | } 388 | 389 | /** 390 | * Sets password on Login.fields.secret and Login.raw, 391 | * leave the secretPrefix unchanged. 392 | * 393 | * @since 3.8.0 394 | * 395 | * @param {string} password Value of password to be assigned. 396 | */ 397 | Login.prototype.setPassword = function(password = "") { 398 | // secret is either entire raw text or defaults to blank string 399 | let secret = this.raw || "" 400 | 401 | // if user has secret prefix make sure it persists 402 | const combined = (this.hasSecretPrefix()) ? `${this.secretPrefix}: ${password}` : password 403 | 404 | // check for an existing prefix + password 405 | const start = secret.search(multiLineSecretRegEx) 406 | if (start > -1) { 407 | // multi line, update the secret/password, not the prefix 408 | const remaining = secret.substring(start) 409 | let end = remaining.search(/[\n\r]/); 410 | end = (end > -1) ? end : remaining.length; // when no newline after pass 411 | 412 | const parts = [ 413 | secret.substring(0, start), 414 | combined, 415 | secret.substring(start + end) 416 | ] 417 | secret = parts.join(""); 418 | } else if (secret.length > 0) { 419 | // replace everything in first line except ending 420 | secret = secret.replace( 421 | /^.*((?:\n\r?))?/, 422 | combined + "$1" 423 | ); 424 | } else { 425 | // when secret is already empty just set password 426 | secret = combined; 427 | } 428 | 429 | this.fields.secret = password; 430 | this.raw = secret; 431 | } 432 | 433 | /** 434 | * Update the raw text details, password, and also the secretPrefix. 435 | * 436 | * @since 3.8.0 437 | * 438 | * @param {string} text Full text details of secret to be updated 439 | */ 440 | Login.prototype.setRawDetails = function (text = "") { 441 | const results = getSecretDetails(text); 442 | 443 | if (results.prefix) { 444 | this.secretPrefix = results.prefix; 445 | } else { 446 | delete this.secretPrefix; 447 | } 448 | this.fields.secret = results.password; 449 | this.raw = text; 450 | } 451 | 452 | module.exports = Login; 453 | -------------------------------------------------------------------------------- /src/popup/models/Settings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("chrome-extension-async"); 4 | 5 | /** 6 | * Settings Constructor() 7 | * @since 3.8.0 8 | * 9 | * @param {object} settingsObj (optional) Extend an existing 10 | * settings object to be backwards and forwards compatible. 11 | */ 12 | function Settings(settingsObj = {}) { 13 | if (Object.prototype.isPrototypeOf(settingsObj)) { 14 | // Set object properties 15 | for (const prop in settingsObj) { 16 | this[prop] = settingsObj[prop]; 17 | } 18 | } 19 | } 20 | 21 | /** 22 | * Check if host application can handle DELETE requests. 23 | * 24 | * @since 3.8.0 25 | * 26 | * @param {object} settingsObj Settings object 27 | * @returns 28 | */ 29 | Settings.prototype.canDelete = function (settingsObj) { 30 | return settingsObj.hasOwnProperty("caps") && settingsObj.caps.delete == true; 31 | } 32 | 33 | /** 34 | * Check if host application can handle SAVE requests. 35 | * 36 | * @since 3.8.0 37 | * 38 | * @param {object} settingsObj Settings object 39 | * @returns 40 | */ 41 | Settings.prototype.canSave = function (settingsObj) { 42 | return settingsObj.hasOwnProperty("caps") && settingsObj.caps.save == true; 43 | } 44 | 45 | /** 46 | * Check if host application can handle TREE requests. 47 | * 48 | * @since 3.8.0 49 | * 50 | * @param {object} settingsObj Settings object 51 | * @returns 52 | */ 53 | Settings.prototype.canTree = function (settingsObj) { 54 | return settingsObj.hasOwnProperty("caps") && settingsObj.caps.tree == true; 55 | } 56 | 57 | /** 58 | * Retrieves Browserpass settings or throws an error. 59 | * Will also cache the first successful response. 60 | * 61 | * @since 3.8.0 62 | * 63 | * @throws {error} Any error response from the host or browser will be thrown 64 | * @returns {object} settings 65 | */ 66 | Settings.prototype.get = async function () { 67 | if (Settings.prototype.isSettings(this.settings)) { 68 | return this.settings 69 | } 70 | 71 | var response = await chrome.runtime.sendMessage({ action: "getSettings" }); 72 | if (response.status != "ok") { 73 | throw new Error(response.message); 74 | } 75 | 76 | // save response to tmp settings variable, sets 77 | let sets = response.settings; 78 | 79 | if (sets.hasOwnProperty("hostError")) { 80 | throw new Error(sets.hostError.params.message); 81 | } 82 | 83 | if (typeof sets.origin === "undefined") { 84 | throw new Error("Unable to retrieve current tab information"); 85 | } 86 | 87 | // cache response.settings for future requests 88 | this.settings = new Settings(sets); 89 | return this.settings; 90 | } 91 | 92 | /** 93 | * Retrieve store object. Can optionally return just the sub path value. 94 | * 95 | * @since 3.8.0 96 | * 97 | * @param {object} settingsObj Settings object 98 | * @param {string} property (optional) store sub property path value to return 99 | * @returns {object} store object or path value 100 | */ 101 | Settings.prototype.getStore = function (settingsObj, property = "") { 102 | let 103 | store = (settingsObj.hasOwnProperty("store")) ? settingsObj.store : {}, 104 | value = null 105 | ; 106 | 107 | switch (property) { 108 | case "color": 109 | case "bgColor": 110 | if (store.hasOwnProperty(property)) { 111 | value = store[property]; 112 | } 113 | break; 114 | 115 | default: 116 | if (property != "" && store.hasOwnProperty(property)) { 117 | value = store[property]; 118 | } else { 119 | value = store; 120 | } 121 | break; 122 | } 123 | 124 | return value; 125 | } 126 | 127 | /** 128 | * Validation, determine if object passed is Settings. 129 | * 130 | * @since 3.8.0 131 | * 132 | * @param {object} settingsObj 133 | * @returns 134 | */ 135 | Settings.prototype.isSettings = function (settingsObj) { 136 | if (typeof settingsObj == 'undefined') { 137 | return false; 138 | } 139 | 140 | return Settings.prototype.isPrototypeOf(settingsObj); 141 | } 142 | 143 | module.exports = Settings 144 | -------------------------------------------------------------------------------- /src/popup/models/Tree.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Tree Constructor() 5 | * 6 | * @since 3.8.0 7 | * 8 | * @param {string} storeId 9 | * @param {array} paths string array of directories relative to 10 | * password store root directory 11 | */ 12 | function Tree(storeId = "", paths = []) { 13 | this.id = storeId; 14 | this.tree = new Map(); 15 | paths.forEach((path) => { 16 | let dirs = path.split("/"); 17 | insert(this.tree, dirs); 18 | }); 19 | } 20 | 21 | /** 22 | * Recursively inserts directories into the Tree 23 | * 24 | * @since 3.8.0 25 | * 26 | * @param {map} parentNode current map instance representing a directory 27 | * in the password store fs Tree. 28 | * @param {array} dirs array of strings for remaining directories 29 | * to be inserted in the Tree. 30 | */ 31 | function insert(parentNode, dirs = []) { 32 | let dir = dirs.shift(); 33 | // done, no more dirs to add 34 | if (dir == undefined) { 35 | return; 36 | } 37 | 38 | // exclude hidden directories 39 | if (dir[0] == ".") { 40 | return 41 | } 42 | 43 | let node = parentNode.get(dir); 44 | 45 | if (node == undefined) { 46 | // doesn't exist, add it 47 | node = new Map(); 48 | parentNode.set(dir, node); 49 | } 50 | 51 | insert(node, dirs); 52 | } 53 | 54 | /** 55 | * Recursively loop over entire tree and return sum of nodes 56 | * 57 | * @since 3.8.0 58 | * 59 | * @param {map} parentNode current map instance, current directory 60 | * @returns {int} sum of all children nodes 61 | */ 62 | function size(parentNode) { 63 | let sum = 0; 64 | parentNode.forEach((node) => { 65 | sum = sum + size(node); 66 | }) 67 | return sum + parentNode.size; 68 | } 69 | 70 | /** 71 | * Sends a 'tree' request to the host application 72 | * @since 3.8.0 73 | * 74 | * @throws {error} host response errors 75 | * 76 | * @param {object} settings Settings object 77 | * @returns {object} object of Trees with storeId as keys 78 | */ 79 | Tree.prototype.getAll = async function(settings) { 80 | // get list of directories 81 | let response = await chrome.runtime.sendMessage({ action: "listDirs" }); 82 | if (response.status != "ok") { 83 | throw new Error(response.message); 84 | } 85 | 86 | let trees = {}; 87 | for (const storeId in response.dirs) { 88 | trees[storeId] = new Tree(storeId, response.dirs[storeId]); 89 | } 90 | return trees; 91 | } 92 | 93 | Tree.prototype.search = function(searchPath = "") { 94 | let paths = searchPath.split("/"); 95 | return searchTree(this.tree, paths); 96 | } 97 | 98 | function searchTree(parentNode, paths) { 99 | let searchTerm = paths.shift(); 100 | // empty search, no matches found 101 | if (searchTerm == undefined) { 102 | return []; 103 | } 104 | 105 | let node = parentNode.get(searchTerm); 106 | 107 | // found exact directory match 108 | let results = [] 109 | if (node != undefined) { 110 | return searchTree(node, paths); 111 | } 112 | 113 | // handle regex symbols 114 | let escapedSearch = searchTerm. 115 | replaceAll(/[!$()*+,-./:?\[\]^{|}.\\]/gu, c => `\\${c}`); 116 | 117 | try { 118 | "".search(escapedSearch) 119 | } catch (error) { 120 | // still need to handle any errors we 121 | // might've missed; catch, log, and stop 122 | console.log(error); 123 | return results; 124 | } 125 | 126 | // no exact match, do fuzzy search 127 | parentNode.forEach((_, dir) => { 128 | if (dir.search(escapedSearch) > -1) { 129 | results.push(dir); 130 | } 131 | }); 132 | return results; 133 | } 134 | 135 | Tree.prototype.isTree = function(treeObj) { 136 | if (typeof treeObj == 'undefined') { 137 | return false; 138 | } 139 | 140 | return Tree.prototype.isPrototypeOf(treeObj); 141 | } 142 | 143 | module.exports = Tree 144 | -------------------------------------------------------------------------------- /src/popup/notifications.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Original source credit goes to github.com/tabula-rasa 3 | * https://gist.github.com/tabula-rasa/61d2ab25aac779fdf9899f4e87ab8306 4 | * with some changes. 5 | */ 6 | 7 | const m = require("mithril"); 8 | const redraw = require("../helpers/redraw"); 9 | const uuidPrefix = RegExp(/^([a-z0-9]){8}-/); 10 | const NOTIFY_CLASS = "m-notifications"; 11 | 12 | /** 13 | * Generate a globally unique id 14 | * 15 | * @since 3.8.0 16 | * 17 | * @returns {string} 18 | */ 19 | function guid() { 20 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { 21 | var r = (Math.random() * 16) | 0, 22 | v = c == "x" ? r : (r & 0x3) | 0x8; 23 | return v.toString(16); 24 | }); 25 | } 26 | 27 | let state = { 28 | list: [], 29 | 30 | /** 31 | * Remove notification vnode from current state 32 | * 33 | * @since 3.8.0 34 | * 35 | * @param {object,string} msg vnode object or uuid string of message to remove 36 | * @returns null 37 | */ 38 | destroy(msg) { 39 | let messageId = ""; 40 | if (typeof msg == "string" && msg.search(uuidPrefix) == 0) { 41 | messageId = msg; 42 | } else if (msg.hasOwnProperty("id") && msg.id.search(uuidPrefix) == 0) { 43 | messageId = msg.id; 44 | } else { 45 | return; 46 | } 47 | 48 | // remove message if index of notification state object is found 49 | let index = state.list.findIndex((x) => x.id === messageId); 50 | if (index > -1) { 51 | state.list.splice(index, 1); 52 | } 53 | }, 54 | }; 55 | 56 | /** 57 | * Creates new notification message and adds it to current 58 | * notification state. 59 | * 60 | * @since 3.8.0 61 | * 62 | * @param {string} text message to display 63 | * @param {number} timeout milliseconds timeout until message automatically removed 64 | * @param {string} type notification message type 65 | * @returns {string} uuid of new message element 66 | */ 67 | function addMessage(text, timeout, type = "info") { 68 | const id = guid(); 69 | state.list.push({ id: id, type: type, text, timeout }); 70 | return id; 71 | } 72 | 73 | function addSuccess(text, timeout = 3500) { 74 | return addMessage(text, timeout, "success"); 75 | } 76 | 77 | function addInfo(text, timeout = 3500) { 78 | return addMessage(text, timeout, "info"); 79 | } 80 | 81 | function addWarning(text, timeout = 4000) { 82 | return addMessage(text, timeout, "warning"); 83 | } 84 | 85 | function addError(text, timeout = 5000) { 86 | return addMessage(text, timeout, "error"); 87 | } 88 | 89 | let Notifications = { 90 | onupdate: function () { 91 | setTimeout(() => { 92 | redraw.increaseModalHeight(document.getElementsByClassName(NOTIFY_CLASS)[0], 25); 93 | }, 25); 94 | }, 95 | view(vnode) { 96 | let ui = vnode.state; 97 | return state.list 98 | ? m( 99 | `.${NOTIFY_CLASS}`, 100 | state.list.map((msg) => { 101 | return m("div", { key: msg.id }, m(Notification, msg)); //wrap in div with key for proper dom updates 102 | }) 103 | ) 104 | : null; 105 | }, 106 | // provide caller method to remove message early 107 | removeMsg(uuid) { 108 | state.destroy(uuid); 109 | m.redraw(); 110 | }, 111 | errorMsg: addError, 112 | infoMsg: addInfo, 113 | successMsg: addSuccess, 114 | warningMsg: addWarning, 115 | }; 116 | 117 | let Notification = { 118 | oninit(vnode) { 119 | if (vnode.attrs.timeout > 0) { 120 | setTimeout(() => { 121 | Notification.destroy(vnode); 122 | }, vnode.attrs.timeout); 123 | } 124 | }, 125 | notificationClass(type) { 126 | const types = ["info", "warning", "success", "error"]; 127 | if (types.indexOf(type) > -1) return type; 128 | return "info"; 129 | }, 130 | destroy(vnode) { 131 | state.destroy(vnode.attrs); 132 | m.redraw(); 133 | }, 134 | view(vnode) { 135 | let ui = vnode.state; 136 | let msg = vnode.attrs; 137 | return m( 138 | ".m-notification", 139 | { 140 | class: ui.notificationClass(msg.type), 141 | onclick: () => { 142 | ui.destroy(vnode); 143 | }, 144 | }, 145 | msg.text 146 | ); 147 | }, 148 | }; 149 | 150 | module.exports = Notifications; 151 | -------------------------------------------------------------------------------- /src/popup/page-loader-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/src/popup/page-loader-dark.gif -------------------------------------------------------------------------------- /src/popup/page-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browserpass/browserpass-extension/32bedd5bec9e3f57f52e20a183832cef40635606/src/popup/page-loader.gif -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Loading available logins...
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/popup/popup.js: -------------------------------------------------------------------------------- 1 | //------------------------------------- Initialisation --------------------------------------// 2 | "use strict"; 3 | 4 | require("chrome-extension-async"); 5 | 6 | // models 7 | const Login = require("./models/Login"); 8 | const Settings = require("./models/Settings"); 9 | // utils, libs 10 | const helpers = require("../helpers/ui"); 11 | const m = require("mithril"); 12 | // components 13 | const AddEditInterface = require("./addEditInterface"); 14 | const DetailsInterface = require("./detailsInterface"); 15 | const Interface = require("./interface"); 16 | const layout = require("./layoutInterface"); 17 | 18 | run(); 19 | 20 | //----------------------------------- Function definitions ----------------------------------// 21 | 22 | /** 23 | * Run the main popup logic 24 | * 25 | * @since 3.0.0 26 | * 27 | * @return void 28 | */ 29 | async function run() { 30 | try { 31 | /** 32 | * Create instance of settings, which will cache 33 | * first request of settings which will be re-used 34 | * for subsequent requests. Pass this settings 35 | * instance pre-cached to each of the views. 36 | */ 37 | let settingsModel = new Settings(); 38 | 39 | // get user settings 40 | var logins = [], 41 | settings = await settingsModel.get(), 42 | root = document.getElementsByTagName("html")[0]; 43 | root.classList.remove("colors-dark"); 44 | root.classList.add(`colors-${settings.theme}`); 45 | 46 | /** 47 | * Only set width: min-content for the attached popup, 48 | * and allow content to fill detached window 49 | */ 50 | if (!Object.prototype.hasOwnProperty.call(settings, "authRequested")) { 51 | root.classList.add("attached"); 52 | document.getElementsByTagName("body")[0].classList.add("attached"); 53 | } 54 | 55 | // set theme 56 | const theme = 57 | settings.theme === "auto" 58 | ? window.matchMedia("(prefers-color-scheme: dark)").matches 59 | ? "dark" 60 | : "light" 61 | : settings.theme; 62 | root.classList.remove("colors-light", "colors-dark"); 63 | root.classList.add(`colors-${theme}`); 64 | 65 | // get list of logins 66 | logins = await Login.prototype.getAll(settings); 67 | layout.setSessionSettings(settings); 68 | // save list of logins to validate when adding 69 | // a new one will not overwrite any existing ones 70 | layout.setStoreLogins(logins.raw); 71 | 72 | const LoginView = new AddEditInterface(settingsModel); 73 | m.route(document.body, "/list", { 74 | "/list": page(new Interface(settings, logins.processed)), 75 | "/details/:storeid/:login": page(new DetailsInterface(settingsModel)), 76 | "/edit/:storeid/:login": page(LoginView), 77 | "/add": page(LoginView), 78 | }); 79 | } catch (e) { 80 | helpers.handleError(e); 81 | } 82 | } 83 | 84 | function page(component) { 85 | return { 86 | render: function (vnode) { 87 | return m(layout.LayoutInterface, m(component, { context: vnode.attrs })); 88 | }, 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/popup/popup.less: -------------------------------------------------------------------------------- 1 | @import "colors-dark.less"; 2 | @import "colors-light.less"; 3 | 4 | @login-height: 53px; 5 | @max-logins-height: @login-height * 7; 6 | @login-part-padding: 6px; 7 | @login-part-height: @login-height - 2 * @login-part-padding; 8 | 9 | @font-face { 10 | font-family: "Open Sans"; 11 | font-style: normal; 12 | font-weight: 400; 13 | src: local("Open Sans"), url("/fonts/OpenSans-Regular.ttf") format("truetype"); 14 | } 15 | 16 | @font-face { 17 | font-family: "Open Sans"; 18 | font-style: normal; 19 | font-weight: 300; 20 | src: local("Open Sans Light"), url("/fonts/OpenSans-Light.ttf") format("truetype"); 21 | } 22 | 23 | @font-face { 24 | font-family: "Source Code Pro"; 25 | font-style: normal; 26 | font-weight: 400; 27 | src: local("Source Code Pro"), url("/fonts/SourceCodePro-Regular.ttf") format("truetype"); 28 | } 29 | 30 | html, 31 | body { 32 | font-family: "Open Sans"; 33 | font-size: 14px; 34 | margin: 0; 35 | padding: 0; 36 | min-width: 260px; 37 | overflow-x: hidden; 38 | white-space: nowrap; 39 | } 40 | 41 | html.attached, 42 | body.attached { 43 | width: min-content; 44 | } 45 | 46 | @media (min-resolution: 192dpi) { 47 | html, 48 | body { 49 | font-weight: 300; 50 | } 51 | } 52 | 53 | html::-webkit-scrollbar, 54 | body::-webkit-scrollbar { 55 | display: none; 56 | } 57 | 58 | body { 59 | display: flex; 60 | flex-direction: column; 61 | } 62 | 63 | .logins { 64 | box-sizing: border-box; 65 | display: flex; 66 | flex-direction: column; 67 | overflow-y: auto; 68 | height: inherit; 69 | max-height: @max-logins-height; 70 | } 71 | 72 | .badge { 73 | display: flex; 74 | align-items: center; 75 | border-radius: 4px; 76 | font-size: 12px; 77 | margin-right: 8px; 78 | padding: 1px 4px; 79 | } 80 | 81 | .details .header { 82 | display: flex; 83 | margin-bottom: 4px; 84 | } 85 | 86 | .part { 87 | box-sizing: border-box; 88 | display: flex; 89 | flex-shrink: 0; 90 | width: 100%; 91 | } 92 | 93 | .part:last-child { 94 | border-bottom: none; 95 | } 96 | 97 | .part > .badge:first-child { 98 | margin-left: 0; 99 | } 100 | 101 | .part.error { 102 | white-space: normal; 103 | padding: 7px; 104 | } 105 | 106 | .part.notice { 107 | white-space: normal; 108 | padding: 7px; 109 | } 110 | 111 | .part.details { 112 | flex-direction: column; 113 | padding: 5px 10px 10px; 114 | & > .part { 115 | display: flex; 116 | margin-bottom: 11px; 117 | &:last-child { 118 | margin-bottom: 0; 119 | } 120 | &.snack { 121 | border: 1px solid; 122 | border-radius: 2px; 123 | height: 36px; 124 | padding: 4px; 125 | .char { 126 | white-space: pre; 127 | } 128 | & > .label { 129 | border-radius: 2px 0 0 2px; 130 | cursor: default; 131 | display: flex; 132 | flex-grow: 0; 133 | font-weight: bold; 134 | justify-content: flex-end; 135 | margin: -5px 8px -5px -5px; 136 | padding: 4px 8px 4px 4px; 137 | width: 3.25em; 138 | } 139 | & > :not(.label) { 140 | display: flex; 141 | align-items: center; 142 | font-family: Source Code Pro, monospace; 143 | } 144 | & > .copy, 145 | & > .generate { 146 | cursor: pointer; 147 | flex-grow: 0; 148 | padding: 0 24px 0 0; 149 | background-position: top 4px right 4px; 150 | background-repeat: no-repeat; 151 | background-size: 16px; 152 | margin: 2px; 153 | } 154 | 155 | & > .copy { 156 | background-image: url("/popup/icon-copy.svg"); 157 | } 158 | 159 | & > .generate { 160 | background-image: url("/popup/icon-generate.svg"); 161 | } 162 | 163 | & > .progress-container { 164 | z-index: 2; 165 | position: absolute; 166 | margin: 30px 0 -4px calc(3.25em + 7px); 167 | height: 1px; 168 | width: calc(100% - 6.5em + 12px); 169 | & > .progress { 170 | height: 100%; 171 | margin: 0; 172 | } 173 | } 174 | } 175 | &.raw textarea { 176 | border: 1px solid; 177 | border-radius: 2px; 178 | flex-grow: 1; 179 | font-family: Source Code Pro, monospace; 180 | min-height: 110px; 181 | min-width: 340px; 182 | outline: none; 183 | padding: 10px; 184 | white-space: pre; 185 | } 186 | & > * { 187 | flex-grow: 1; 188 | align-items: center; 189 | } 190 | } 191 | } 192 | 193 | .part.search { 194 | padding: 6px 28px 6px 6px; 195 | background-image: url("/popup/icon-search.svg"); 196 | background-position: top 6px right 6px; 197 | background-repeat: no-repeat; 198 | background-size: 18px; 199 | } 200 | 201 | .part.search > .hint { 202 | line-height: 19px; 203 | } 204 | 205 | .part.search > .hint > .remove-hint { 206 | background-image: url("/popup/icon-bs-delete.svg"); 207 | background-repeat: no-repeat; 208 | background-size: contain; 209 | cursor: pointer; 210 | height: 12px; 211 | margin: 3px 0 3px 4px; 212 | width: 16px; 213 | } 214 | 215 | .part.search > input[type="text"] { 216 | border: none; 217 | outline: none; 218 | width: 100%; 219 | font-family: "Open Sans"; 220 | } 221 | 222 | .part.search > input[type="text"]::placeholder { 223 | opacity: 1; 224 | } 225 | 226 | .part.login > .name:hover, 227 | .part.login > .name:focus, 228 | .part.login > .action:hover, 229 | .part.login > .action:focus, 230 | .part.login:focus > .name { 231 | outline: none; 232 | } 233 | 234 | .part.login { 235 | display: flex; 236 | align-items: center; 237 | height: @login-height; 238 | 239 | &.details-header { 240 | height: calc(@login-height + 6px); 241 | padding: 0 4px; 242 | outline: none; 243 | } 244 | 245 | &:not(.details-header) { 246 | cursor: pointer; 247 | } 248 | 249 | &:hover, 250 | &:focus { 251 | outline: none; 252 | } 253 | 254 | .name { 255 | display: flex; 256 | flex-direction: column; 257 | width: 100%; 258 | height: @login-part-height; 259 | padding: @login-part-padding; 260 | 261 | .line1 { 262 | display: flex; 263 | flex-direction: row; 264 | font-size: 12px; 265 | 266 | .recent { 267 | background-image: url("/popup/icon-history.svg"); 268 | background-repeat: no-repeat; 269 | background-size: contain; 270 | margin-left: 8px; 271 | width: 9.5px; 272 | margin-top: 4px; 273 | } 274 | } 275 | 276 | .line2 { 277 | font-size: 18px; 278 | margin-top: 2px; 279 | } 280 | } 281 | 282 | .action { 283 | background-position: center; 284 | background-repeat: no-repeat; 285 | background-size: 19px; 286 | cursor: pointer; 287 | width: 30px; 288 | height: @login-part-height; 289 | padding: @login-part-padding; 290 | 291 | &.back { 292 | background-image: url("/popup/icon-back.svg"); 293 | } 294 | 295 | &.copy-password { 296 | background-image: url("/popup/icon-key.svg"); 297 | } 298 | 299 | &.copy-user { 300 | background-image: url("/popup/icon-user.svg"); 301 | } 302 | 303 | &.details { 304 | background-image: url("/popup/icon-details.svg"); 305 | } 306 | 307 | &.edit { 308 | background-image: url("/popup/icon-edit.svg"); 309 | } 310 | 311 | &.save { 312 | background-image: url("/popup/icon-save.svg"); 313 | } 314 | 315 | &.delete { 316 | background-image: url("/popup/icon-delete.svg"); 317 | } 318 | } 319 | } 320 | 321 | .part.login em { 322 | font-style: normal; 323 | } 324 | 325 | .part.add { 326 | display: flex; 327 | cursor: pointer; 328 | align-items: center; 329 | justify-content: center; 330 | height: 30px; 331 | } 332 | 333 | .chars { 334 | display: flex; 335 | flex-grow: 1; 336 | align-items: center; 337 | font-family: Source Code Pro, monospace; 338 | .char { 339 | white-space: pre; 340 | } 341 | } 342 | 343 | .addEdit, 344 | .details { 345 | .loading { 346 | width: 300px; 347 | height: 300px; 348 | display: flex; 349 | align-items: flex-end; 350 | p { 351 | margin: 50px auto; 352 | font-size: large; 353 | } 354 | } 355 | 356 | .title { 357 | display: flex; 358 | box-sizing: border-box; 359 | // justify-content: space-between; 360 | 361 | span { 362 | // align-self: center; 363 | margin: auto; 364 | } 365 | } 366 | 367 | .btn { 368 | cursor: pointer; 369 | background-position: center; 370 | background-repeat: no-repeat; 371 | padding: 6px; 372 | background-size: 19px; 373 | height: 21px; 374 | 375 | &:hover, 376 | &:focus { 377 | outline: none; 378 | } 379 | 380 | &.back { 381 | background-image: url("/popup/icon-back.svg"); 382 | width: 20px; 383 | } 384 | 385 | &.edit { 386 | background-image: url("/popup/icon-edit.svg"); 387 | width: 25px; 388 | } 389 | 390 | &.alignment { 391 | width: 20px; 392 | cursor: default; 393 | background: none !important; 394 | } 395 | 396 | &.generate { 397 | width: 15px; 398 | padding-right: 10px; 399 | background-image: url("/popup/icon-generate.svg"); 400 | } 401 | } 402 | 403 | .location { 404 | margin: 6px; 405 | 406 | .store, 407 | .path { 408 | display: flex; 409 | align-items: center; 410 | margin-bottom: 6px; 411 | padding-right: 5px; 412 | } 413 | 414 | .store { 415 | select { 416 | border-radius: 4px; 417 | } 418 | } 419 | 420 | .storePath { 421 | margin-left: 10px; 422 | font-size: 12px; 423 | } 424 | 425 | .path { 426 | border: 1px solid; 427 | 428 | input { 429 | padding-right: 0; 430 | } 431 | } 432 | 433 | .suffix { 434 | font-size: 13px; 435 | font-family: Source Code Pro, monospace; 436 | } 437 | 438 | select { 439 | border: none; 440 | outline: none; 441 | padding: 5px; 442 | cursor: pointer; 443 | } 444 | 445 | input[disabled], 446 | select[disabled], 447 | .suffix.disabled { 448 | color: grey; 449 | cursor: default; 450 | } 451 | } 452 | 453 | .contents { 454 | display: flex; 455 | flex-direction: column; 456 | margin: 0 6px 5px; 457 | 458 | label { 459 | display: flex; 460 | flex-grow: 0; 461 | justify-content: flex-end; 462 | font-weight: bold; 463 | padding: 6px 6px; 464 | margin-right: 8px; 465 | border: none; 466 | min-width: 55px; 467 | } 468 | 469 | .password, 470 | .options, 471 | .details { 472 | display: flex; 473 | margin-bottom: 6px; 474 | border-radius: 2px; 475 | } 476 | 477 | .password, 478 | .details { 479 | border: 1px solid; 480 | } 481 | 482 | .options { 483 | align-items: center; 484 | 485 | label { 486 | margin-right: 0; 487 | } 488 | 489 | input[type="checkbox"] { 490 | margin: 3px 6px; 491 | height: 25px; 492 | width: 20px; 493 | } 494 | 495 | input[type="number"] { 496 | font-size: 12px; 497 | width: 40px; 498 | height: 20px; 499 | } 500 | } 501 | } 502 | 503 | .actions { 504 | display: flex; 505 | margin-bottom: 10px; 506 | 507 | .save, 508 | .delete { 509 | cursor: pointer; 510 | font-weight: bolder; 511 | font-size: medium; 512 | padding: 0.25rem 0.75rem; 513 | border-radius: 0.25rem; 514 | } 515 | 516 | .save::after, 517 | .delete::after { 518 | display: inline-block; 519 | width: 15px; 520 | margin-left: 5px; 521 | } 522 | 523 | .save { 524 | margin-left: auto; 525 | margin-right: 5px; 526 | } 527 | .save::after { 528 | content: url("/popup/icon-save.svg"); 529 | } 530 | 531 | .delete { 532 | margin-left: 5px; 533 | margin-right: auto; 534 | } 535 | .delete::after { 536 | content: url("/popup/icon-delete.svg"); 537 | } 538 | } 539 | 540 | input[type="number"], 541 | input[type="text"], 542 | textarea { 543 | border: none; 544 | outline: none; 545 | width: 100%; 546 | font-family: "Open Sans"; 547 | padding: 5px 8px; 548 | font-family: Source Code Pro, monospace; 549 | } 550 | 551 | textarea { 552 | resize: none; 553 | min-height: 110px; 554 | min-width: 340px; 555 | } 556 | } 557 | 558 | .updates { 559 | padding: @login-part-padding; 560 | } 561 | 562 | .m-notifications { 563 | position: fixed; 564 | top: 20px; 565 | left: 0; 566 | display: flex; 567 | flex-direction: column; 568 | align-items: flex-start; 569 | z-index: 10; 570 | 571 | .m-notification { 572 | width: auto; 573 | margin: 0 10px 3px; 574 | cursor: pointer; 575 | animation: fade-in 0.3s; 576 | white-space: normal; 577 | // display errors in notifications nicely 578 | overflow-wrap: anywhere; 579 | 580 | &.destroy { 581 | animation: fade-out 0.3s; 582 | } 583 | 584 | &.info, 585 | &.warning, 586 | &.error, 587 | &.success { 588 | padding: 0.75rem 1.25rem; 589 | border-radius: 0.25rem; 590 | } 591 | } 592 | } 593 | 594 | div#tree-dirs { 595 | position: absolute; 596 | overflow-y: scroll; 597 | max-height: 265px; 598 | margin-top: -8px; 599 | 600 | div.dropdown { 601 | padding: 2px; 602 | 603 | a { 604 | display: block; 605 | text-align: left; 606 | padding: 2px 6px; 607 | } 608 | } 609 | } 610 | 611 | dialog#browserpass-modal { 612 | margin: auto 6px; 613 | width: auto; 614 | border-radius: 0.25rem; 615 | 616 | .modal-content { 617 | margin-bottom: 15px; 618 | white-space: pre-wrap; 619 | 620 | p { 621 | margin: 0; 622 | } 623 | } 624 | 625 | .modal-actions { 626 | display: flex; 627 | 628 | button { 629 | font-weight: bold; 630 | padding: 5px 10px; 631 | border-radius: 0.25rem; 632 | 633 | &.cancel { 634 | margin-right: auto; 635 | } 636 | &.confirm { 637 | margin-left: auto; 638 | } 639 | } 640 | } 641 | } 642 | 643 | @keyframes fade-in { 644 | from { 645 | opacity: 0; 646 | } 647 | 648 | to { 649 | opacity: 1; 650 | } 651 | } 652 | 653 | @keyframes fade-out { 654 | from { 655 | opacity: 1; 656 | } 657 | 658 | to { 659 | opacity: 0; 660 | } 661 | } 662 | 663 | .generate-heights(@start, @end, @i: 0, @step: 25) when ((@start + @i * @step) =< @end) { 664 | .mh-@{i} { 665 | min-height: (1px * @start) + (1px * @i * @step); 666 | } 667 | .generate-heights(@start, @end, (@i + 1)); 668 | } 669 | .generate-heights(300, 1000); 670 | -------------------------------------------------------------------------------- /src/popup/searchinterface.js: -------------------------------------------------------------------------------- 1 | module.exports = SearchInterface; 2 | 3 | const BrowserpassURL = require("@browserpass/url"); 4 | const dialog = require("./modalDialog"); 5 | const helpers = require("../helpers/base"); 6 | const helpersUI = require("../helpers/ui"); 7 | const m = require("mithril"); 8 | 9 | /** 10 | * Search interface 11 | * 12 | * @since 3.0.0 13 | * 14 | * @param object interface Popup main interface 15 | * @return void 16 | */ 17 | function SearchInterface(popup) { 18 | // public methods 19 | this.view = view; 20 | 21 | // fields 22 | this.popup = popup; 23 | } 24 | 25 | /** 26 | * Generates vnodes for render 27 | * 28 | * @since 3.0.0 29 | * 30 | * @param function ctl Controller 31 | * @param object params Runtime params 32 | * @return []Vnode 33 | */ 34 | function view(ctl, params) { 35 | var self = this; 36 | 37 | const url = helpersUI.getCurrentUrl(this.popup.settings); 38 | const host = url.isValid ? url.hostname : ""; 39 | 40 | return m( 41 | "form.part.search", 42 | { 43 | onkeydown: function (e) { 44 | switch (e.code) { 45 | case "Tab": 46 | e.preventDefault(); 47 | if (e.shiftKey) { 48 | document.querySelector(".part.add").focus(); 49 | break; 50 | } 51 | // fall through to ArrowDown 52 | case "ArrowDown": 53 | e.preventDefault(); 54 | if (self.popup.results.length) { 55 | document.querySelector("*[tabindex]").focus(); 56 | } 57 | break; 58 | case "ArrowUp": 59 | e.preventDefault(); 60 | document.querySelector(".part.add").focus(); 61 | break; 62 | case "End": 63 | if (!e.shiftKey) { 64 | e.preventDefault(); 65 | document.querySelector(".part.add").focus(); 66 | } 67 | break; 68 | case "Enter": 69 | e.preventDefault(); 70 | if (self.popup.results.length) { 71 | self.popup.results[0].doAction("fill"); 72 | } 73 | break; 74 | } 75 | }, 76 | }, 77 | [ 78 | this.popup.currentDomainOnly 79 | ? m("div.hint.badge", [ 80 | host, 81 | m("div.remove-hint", { 82 | title: "Clear domain filter | ", 83 | onclick: function (e) { 84 | var target = document.querySelector( 85 | ".part.search > input[type=text]" 86 | ); 87 | target.focus(); 88 | self.popup.currentDomainOnly = false; 89 | self.popup.search(target.value); 90 | }, 91 | }), 92 | ]) 93 | : null, 94 | m("input[type=text]", { 95 | focused: true, 96 | placeholder: "Search logins...", 97 | oncreate: function (e) { 98 | e.dom.focus(); 99 | }, 100 | oninput: function (e) { 101 | self.popup.search(e.target.value); 102 | }, 103 | onkeydown: function (e) { 104 | switch (e.code) { 105 | case "Backspace": 106 | if (self.popup.currentDomainOnly) { 107 | if (e.target.value.length == 0) { 108 | self.popup.currentDomainOnly = false; 109 | self.popup.search(""); 110 | } else if ( 111 | e.target.selectionStart == 0 && 112 | e.target.selectionEnd == 0 113 | ) { 114 | self.popup.currentDomainOnly = false; 115 | self.popup.search(e.target.value); 116 | } 117 | } 118 | break; 119 | case "KeyC": 120 | if (e.ctrlKey && e.target.selectionStart == e.target.selectionEnd) { 121 | e.preventDefault(); 122 | self.popup.results[0].doAction( 123 | e.shiftKey ? "copyUsername" : "copyPassword" 124 | ); 125 | } 126 | break; 127 | case "KeyG": 128 | if (e.ctrlKey && e.target.selectionStart == e.target.selectionEnd) { 129 | e.preventDefault(); 130 | const event = e; 131 | const target = self.popup.results[0]; 132 | dialog.open( 133 | helpers.LAUNCH_URL_DEPRECATION_MESSAGE, 134 | function () { 135 | target.doAction( 136 | event.shiftKey ? "launchInNewTab" : "launch" 137 | ); 138 | }, 139 | false 140 | ); 141 | } 142 | break; 143 | case "KeyO": 144 | if (e.ctrlKey && e.target.selectionStart == e.target.selectionEnd) { 145 | e.preventDefault(); 146 | self.popup.results[0].doAction("getDetails"); 147 | } 148 | break; 149 | case "End": { 150 | if (e.target.selectionStart === e.target.value.length) { 151 | let logins = document.querySelectorAll(".login"); 152 | if (logins.length) { 153 | let target = logins.item(logins.length - 1); 154 | target.focus(); 155 | target.scrollIntoView(); 156 | } 157 | } 158 | break; 159 | } 160 | } 161 | }, 162 | }), 163 | ] 164 | ); 165 | } 166 | --------------------------------------------------------------------------------