├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ ├── codeql-analysis.yml │ ├── i18n.yml │ ├── main.yml │ ├── release.yml │ └── tagging.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── _locales ├── ar │ └── messages.json ├── bg │ └── messages.json ├── bn │ └── messages.json ├── ca │ └── messages.json ├── cs │ └── messages.json ├── da │ └── messages.json ├── de │ └── messages.json ├── el │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── et │ └── messages.json ├── fa │ └── messages.json ├── fi │ └── messages.json ├── fr │ └── messages.json ├── fy │ └── messages.json ├── he │ └── messages.json ├── hi │ └── messages.json ├── hr │ └── messages.json ├── hu │ └── messages.json ├── hy │ └── messages.json ├── id │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── ka │ └── messages.json ├── kaa │ └── messages.json ├── ko │ └── messages.json ├── lt │ └── messages.json ├── lv │ └── messages.json ├── nl │ └── messages.json ├── no │ └── messages.json ├── pl │ └── messages.json ├── pt │ └── messages.json ├── pt_BR │ └── messages.json ├── ro │ └── messages.json ├── ru │ └── messages.json ├── sq │ └── messages.json ├── sr │ └── messages.json ├── sv │ └── messages.json ├── th │ └── messages.json ├── tr │ └── messages.json ├── uk │ └── messages.json ├── vi │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── crowdin.yml ├── images ├── icon.svg ├── icon128.png ├── icon16.png ├── icon19.png ├── icon38.png ├── icon48.png └── scan.gif ├── manifests ├── manifest-chrome-testing.json ├── manifest-chrome.json ├── manifest-edge.json ├── manifest-firefox-testing.json ├── manifest-firefox.json ├── manifest-pwa.json └── schema-chrome.json ├── package-lock.json ├── package.json ├── sass ├── DroidSansMono.woff2 ├── _ui.scss ├── content.scss ├── import.scss ├── mocha.css ├── permissions.scss └── popup.scss ├── scripts ├── build.sh ├── credentials.ts.gpg ├── deploy-key.gpg ├── i18n.js ├── i18n.sh ├── licenses-template.html ├── release.sh ├── tag.sh └── test-runner.ts ├── src ├── argon.ts ├── background.ts ├── browser.ts ├── components │ ├── Import.vue │ ├── Import │ │ ├── FileImport.vue │ │ ├── QrImport.vue │ │ └── TextImport.vue │ ├── Options.vue │ ├── Permissions.vue │ ├── Popup.vue │ ├── Popup │ │ ├── AddAccountPage.vue │ │ ├── AddMethodPage.vue │ │ ├── AdvisorInsight.vue │ │ ├── AdvisorPage.vue │ │ ├── BackupPage.vue │ │ ├── DrivePage.vue │ │ ├── DropboxPage.vue │ │ ├── EnterPasswordPage.vue │ │ ├── EntryComponent.vue │ │ ├── LoadingPage.vue │ │ ├── MainBody.vue │ │ ├── MainHeader.vue │ │ ├── MenuPage.vue │ │ ├── NotificationHandler.vue │ │ ├── OneDrivePage.vue │ │ ├── PageHandler.vue │ │ ├── PreferencesPage.vue │ │ └── SetPasswordPage.vue │ └── common │ │ ├── ButtonInput.vue │ │ ├── ButtonLink.vue │ │ ├── FileInput.vue │ │ ├── SelectInput.vue │ │ ├── TextInput.vue │ │ ├── ToggleInput.vue │ │ └── index.ts ├── content.ts ├── definitions │ ├── BackupProvider.d.ts │ ├── advisor.d.ts │ ├── gost.d.ts │ ├── i18n.d.ts │ ├── module-interface.d.ts │ ├── otp.d.ts │ ├── permission.d.ts │ ├── shims-vue.d.ts │ ├── vue.d.ts │ └── vue2-dragula.d.ts ├── import.ts ├── mochaReporter.ts ├── models │ ├── advisor.ts │ ├── backup.ts │ ├── credentials.ts │ ├── encryption.ts │ ├── key-utilities.ts │ ├── migration.ts │ ├── otp.ts │ ├── password.ts │ ├── permission.ts │ ├── settings.ts │ └── storage.ts ├── options.ts ├── permissions.ts ├── popup.ts ├── qrdebug.ts ├── store │ ├── Accounts.ts │ ├── Advisor.ts │ ├── Backup.ts │ ├── CurrentView.ts │ ├── Menu.ts │ ├── Notification.ts │ ├── Permissions.ts │ ├── Qr.ts │ ├── Style.ts │ └── i18n.ts ├── syncTime.ts ├── test.ts ├── test │ ├── components │ │ └── Popup │ │ │ ├── EnterPasswordPage.test.ts │ │ │ └── MenuPage.test.ts │ ├── gost.test.ts │ └── test.ts.disabled └── utils.ts ├── svg ├── arrow-left.svg ├── bars.svg ├── check.svg ├── clipboard-check.svg ├── code.svg ├── cog.svg ├── comments.svg ├── database.svg ├── exchange.svg ├── globe.svg ├── info.svg ├── key-solid.svg ├── lightbulb.svg ├── lock.svg ├── minus-circle.svg ├── pencil.svg ├── pin.svg ├── plus.svg ├── qrcode.svg ├── redo.svg ├── scan.svg ├── sync.svg ├── wrench.svg └── x-circle.svg ├── tsconfig.json ├── view ├── argon.html ├── import.html ├── options.html ├── permissions.html ├── popup.html ├── qrdebug.html └── test.html ├── webpack.config.js ├── webpack.dev.js ├── webpack.prod.js └── webpack.watch.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | firefox 4 | chrome 5 | scripts 6 | webpack.config.js 7 | webpack.prod.js 8 | webpack.dev.js 9 | webpack.watch.js 10 | src/test 11 | src/models/credentials.ts 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | parserOptions: { 5 | ecmaVersion: 6, 6 | }, 7 | plugins: ["@typescript-eslint"], 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | ], 13 | env: { 14 | amd: true, 15 | node: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-use-before-define": "off", 19 | "@typescript-eslint/explicit-function-return-type": "off", 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-vendored 2 | *.css linguist-vendored 3 | _locales/* linguist-generated=true 4 | _locales/en/messages.json linguist-generated=false 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug in Authenticator 3 | labels: 4 | - bug 5 | - unconfirmed 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Do not use this form to ask about lost codes or accounts! 11 | Read more [here](https://authenticator.cc/docs/en/lost-codes). 12 | - type: textarea 13 | attributes: 14 | label: Describe the issue 15 | description: Include detailed steps to reproduce the issue if appropriate. 16 | validations: 17 | required: true 18 | - type: dropdown 19 | attributes: 20 | label: Browser 21 | options: 22 | - Chrome 23 | - Firefox 24 | - Edge 25 | - Other 26 | validations: 27 | required: true 28 | - type: input 29 | attributes: 30 | label: Browser Version 31 | validations: 32 | required: true 33 | - type: input 34 | attributes: 35 | label: Extension Version 36 | description: The extension version is found by clicking the cog at the top left and looking at the bottom of the page. 37 | validations: 38 | required: true 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Feature Request 3 | url: https://github.com/Authenticator-Extension/Authenticator/discussions/new 4 | about: Please request features here. 5 | - name: Translation Issues 6 | url: https://crowdin.com/project/authenticator-firefox 7 | about: Please correct translation issues on Crowdin. 8 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [dev, release] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [dev] 9 | schedule: 10 | - cron: '0 12 * * 4' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /.github/workflows/i18n.yml: -------------------------------------------------------------------------------- 1 | name: i18n 2 | 3 | on: 4 | push: 5 | branches: [ dev ] 6 | 7 | jobs: 8 | i18n-strings: 9 | runs-on: ubuntu-latest 10 | name: Process new i18n strings 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - run: bash scripts/i18n.sh 16 | env: 17 | DEPLOY_KEY_PASSWORD: ${{ secrets.DEPLOY_KEY_PASSWORD }} -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | style: 7 | runs-on: ubuntu-latest 8 | name: Style checks 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Setup Node.js environment 14 | uses: actions/setup-node@v2.1.2 15 | 16 | - name: Install dependencies 17 | run: | 18 | npm i prettier 19 | 20 | - name: Prettier 21 | run: ./node_modules/prettier/bin-prettier.js --check ./src/* ./src/**/* ./src/**/**/* ./sass/*.scss 22 | 23 | build: 24 | runs-on: ubuntu-latest 25 | name: Build ${{ matrix.platform }} 26 | 27 | strategy: 28 | matrix: 29 | platform: ["chrome", "firefox"] 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | 34 | - name: Setup Node.js environment 35 | uses: actions/setup-node@v2.1.2 36 | 37 | - name: Install dependencies 38 | run: npm ci 39 | 40 | - name: Build 41 | run: npm run ${{ matrix.platform }} 42 | run-tests: 43 | runs-on: ubuntu-latest 44 | name: Run tests 45 | needs: [build] 46 | 47 | steps: 48 | - uses: actions/checkout@v2 49 | 50 | - name: Setup Node.js environment 51 | uses: actions/setup-node@v2.1.2 52 | 53 | - name: Install dependencies 54 | run: npm ci 55 | env: 56 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: "true" 57 | 58 | - name: Test code 59 | uses: mymindstorm/puppeteer-headful@8f745c770f7f4c0f9f332d7c43a775f90e53779a 60 | with: 61 | args: npm test 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | name: Publish release build 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup Node.js environment 17 | uses: actions/setup-node@v2.1.2 18 | 19 | - name: Install dependencies 20 | run: npm ci 21 | 22 | - name: Build 23 | run: bash scripts/release.sh 24 | env: 25 | CREDS_FILE_PASSWORD: ${{ secrets.CREDS_FILE_PASSWORD }} 26 | 27 | - name: Create a release 28 | id: create_release 29 | uses: actions/create-release@v1.1.4 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | tag_name: ${{ github.ref }} 34 | release_name: Release ${{ github.ref }} 35 | draft: false 36 | prerelease: false 37 | - name: Upload Release Asset 38 | id: upload-release-asset 39 | uses: actions/upload-release-asset@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | upload_url: ${{ steps.create_release.outputs.upload_url }} 44 | asset_path: ./release.tar.gz 45 | asset_name: release.tar.gz 46 | asset_content_type: application/gzip 47 | -------------------------------------------------------------------------------- /.github/workflows/tagging.yml: -------------------------------------------------------------------------------- 1 | name: Tagging 2 | 3 | on: 4 | push: 5 | branches: [ release ] 6 | 7 | jobs: 8 | tagging: 9 | runs-on: ubuntu-latest 10 | name: Release tagging 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - run: bash scripts/tag.sh 16 | env: 17 | DEPLOY_KEY_PASSWORD: ${{ secrets.DEPLOY_KEY_PASSWORD }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | chrome* 4 | firefox* 5 | edge* 6 | dist 7 | .vscode 8 | .atom-build.yml 9 | scripts/authenticator-build-key.enc 10 | cert.pfx 11 | css 12 | .license-gen-tmp 13 | view/licenses.html 14 | ./test 15 | scripts/authenticator-build-key 16 | scripts/test-runner.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Authenticator Extension 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authenticator [![Build Status](https://travis-ci.com/Authenticator-Extension/Authenticator.svg?branch=dev)](https://travis-ci.com/Authenticator-Extension/Authenticator) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/authenticator-firefox/localized.svg)](https://crowdin.com/project/authenticator-firefox) 2 | 3 | > Authenticator generates 2-Step Verification codes in your browser. 4 | 5 | ## Available for Chrome, Firefox, and Microsoft Edge 6 | 7 | [](https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai) [](https://addons.mozilla.org/en-US/firefox/addon/auth-helper?src=external-github) [](https://microsoftedge.microsoft.com/addons/detail/ocglkepbibnalbgmbachknglpdipeoio) 8 | 9 | 10 | ### Safari Edition 11 | 12 | A Safari edition of Authenticator is available on the App Store. We do not provide official support for the Safari edition. 13 | 14 | [Download on the App Store](https://apps.apple.com/us/app/authen/id1602945200?mt=12) 15 | 16 | ## Build Setup 17 | 18 | ``` bash 19 | # install development dependencies 20 | npm install 21 | # compile 22 | npm run [chrome, firefox, prod] 23 | ``` 24 | 25 | To reproduce a build: 26 | 27 | ``` bash 28 | npm ci 29 | npm run prod 30 | ``` 31 | 32 | To reproduce a build for Safari, please follow contribution guidance in [Authenticator-Extension/Authen](https://github.com/Authenticator-Extension/Authen#how-to-contribute) 33 | 34 | ## Development (Chrome) 35 | 36 | ``` bash 37 | # install development dependencies 38 | npm install 39 | # compiles the Chrome extension to the `./test/chrome` directory 40 | npm run dev:chrome 41 | # load the unpacked extension from the `./test/chrome/ directory in Chrome 42 | ``` 43 | 44 | Note that Windows users should download a tool like [Git Bash](https://git-scm.com/download/win) or [Cygwin](http://cygwin.com/) to build. 45 | 46 | ## Acknowledgment 47 | 48 | We would like to extend our heartfelt thanks to Laurent, the Chief Information Security Officer (CISO) of the University of Luxembourg, for the invaluable support and contribution to this project. During the development process, the CISO team provided critical security recommendations that helped us identify and address potential vulnerabilities, significantly enhancing the security and reliability of the project. 49 | 50 | We especially want to acknowledge the University of Luxembourg's information security team for their selfless contribution, which not only facilitated the progress of this project but also had a positive impact on the broader open-source community. We recognize that the success of open-source software depends heavily on collaboration and support from various stakeholders, and the involvement of the University of Luxembourg has allowed us to offer a more secure and dependable product to a wider audience. 51 | 52 | We understand that while open-source software is free, maintaining and improving these projects requires significant resources. The University of Luxembourg’s information security team has demonstrated their strong commitment to the open-source community, contributing not just within their university but to users and developers globally. We hope this acknowledgment will help them continue to secure the support and resources necessary to further advance open-source initiatives. 53 | 54 | Once again, we express our sincere gratitude to the University of Luxembourg's CISO team for their valuable advice and assistance. 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We support the latest versions published on the Chrome, Firefox, and Edge extension stores. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Report potential vulnerabilities privately via [this form](https://github.com/Authenticator-Extension/Authenticator/security/advisories/new). 10 | Where appropriate, include a proof-of-concept and reproduction steps. 11 | We strive to provide an initial response within five days, but as this is a volunteer-run project, we make no guarantees. -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /_locales/en/messages.json 3 | translation: /_locales/%two_letters_code%/messages.json 4 | languages_mapping: 5 | two_letters_code: 6 | zh-CN: zh_CN 7 | zh-TW: zh_TW 8 | pt-BR: pt_BR 9 | -------------------------------------------------------------------------------- /images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/images/icon128.png -------------------------------------------------------------------------------- /images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/images/icon16.png -------------------------------------------------------------------------------- /images/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/images/icon19.png -------------------------------------------------------------------------------- /images/icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/images/icon38.png -------------------------------------------------------------------------------- /images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/images/icon48.png -------------------------------------------------------------------------------- /images/scan.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/images/scan.gif -------------------------------------------------------------------------------- /manifests/manifest-chrome-testing.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_extName__", 4 | "short_name": "__MSG_extShortName__", 5 | "version": "6.3.4", 6 | "default_locale": "en", 7 | "description": "__MSG_extDesc__", 8 | "icons": { 9 | "16": "images/icon16.png", 10 | "48": "images/icon48.png", 11 | "128": "images/icon128.png" 12 | }, 13 | "action": { 14 | "default_icon": { 15 | "19": "images/icon19.png", 16 | "38": "images/icon38.png" 17 | }, 18 | "default_title": "__MSG_extShortName__", 19 | "default_popup": "view/popup.html" 20 | }, 21 | "commands": { 22 | "_execute_action": {}, 23 | "scan-qr": { 24 | "description": "Scan a QR code" 25 | }, 26 | "autofill": { 27 | "description": "Autofill the matched code" 28 | } 29 | }, 30 | "options_ui": { 31 | "page": "view/options.html", 32 | "open_in_tab": false 33 | }, 34 | "storage": { 35 | "managed_schema": "schema.json" 36 | }, 37 | "oauth2": { 38 | "client_id": "292457304165-u8ve4j79gag5o231n5u2pdtdrbfdo1hh.apps.googleusercontent.com", 39 | "scopes": [ 40 | "https://www.googleapis.com/auth/drive.file" 41 | ] 42 | }, 43 | "background": { 44 | "service_worker": "dist/background.js" 45 | }, 46 | "sandbox": { 47 | "pages": [ 48 | "view/argon.html" 49 | ] 50 | }, 51 | "permissions": [ 52 | "activeTab", 53 | "storage", 54 | "identity", 55 | "alarms", 56 | "scripting" 57 | ], 58 | "optional_permissions": [ 59 | "clipboardWrite", 60 | "contextMenus" 61 | ], 62 | "optional_host_permissions": [ 63 | "https://www.google.com/", 64 | "https://*.dropboxapi.com/*", 65 | "https://www.googleapis.com/*", 66 | "https://accounts.google.com/o/oauth2/revoke", 67 | "https://graph.microsoft.com/me/*", 68 | "https://login.microsoftonline.com/common/oauth2/v2.0/token" 69 | ], 70 | "content_security_policy": { 71 | "extension_pages": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com https://www.googleapis.com/ https://accounts.google.com/o/oauth2/revoke https://login.microsoftonline.com/common/oauth2/v2.0/token https://graph.microsoft.com/; default-src 'none'" 72 | }, 73 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjo5++7m6mlJGqKOnlYehr9tjIqahMZBJUG7PLa7dSRk6bDUu2pVodO1TQWviHlrDTLP+zfoVbDBS8v8cjloK5Tn90nzC6a957dPzOfyC1WUNYNDlGM0BCmZKVP/MWB3d0ffOmTwaxh0L47aLH5nTW0AUmuwCWCBEEl4Acuyp7rwLNGlazBpaom1Qb5ckn29gCJVVVIZ6wudmcrG/FPTNJXQbg8N6wObGrgGOaxmowbkzJmIfKTyHlYOKLAjZ7aJi0W6jsy47/aV+ojvn4gO+ka6BcRhUeWgoQxqEky119f3OWiVP46SJVbAi0pkknThUjDvX11lATGjB5EvJZGyotwIDAQAB" 74 | } 75 | -------------------------------------------------------------------------------- /manifests/manifest-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_extName__", 4 | "short_name": "__MSG_extShortName__", 5 | "version": "8.0.2", 6 | "default_locale": "en", 7 | "description": "__MSG_extDesc__", 8 | "icons": { 9 | "16": "images/icon16.png", 10 | "48": "images/icon48.png", 11 | "128": "images/icon128.png" 12 | }, 13 | "action": { 14 | "default_icon": { 15 | "19": "images/icon19.png", 16 | "38": "images/icon38.png" 17 | }, 18 | "default_title": "__MSG_extShortName__", 19 | "default_popup": "view/popup.html" 20 | }, 21 | "commands": { 22 | "_execute_action": {}, 23 | "scan-qr": { 24 | "description": "Scan a QR code" 25 | }, 26 | "autofill": { 27 | "description": "Autofill the matched code" 28 | } 29 | }, 30 | "options_ui": { 31 | "page": "view/options.html", 32 | "open_in_tab": false 33 | }, 34 | "storage": { 35 | "managed_schema": "schema.json" 36 | }, 37 | "oauth2": { 38 | "client_id": "292457304165-u8ve4j79gag5o231n5u2pdtdrbfdo1hh.apps.googleusercontent.com", 39 | "scopes": [ 40 | "https://www.googleapis.com/auth/drive.file" 41 | ] 42 | }, 43 | "background": { 44 | "service_worker": "dist/background.js" 45 | }, 46 | "sandbox": { 47 | "pages": [ 48 | "view/argon.html" 49 | ] 50 | }, 51 | "permissions": [ 52 | "activeTab", 53 | "storage", 54 | "identity", 55 | "alarms", 56 | "scripting" 57 | ], 58 | "optional_permissions": [ 59 | "clipboardWrite", 60 | "contextMenus" 61 | ], 62 | "optional_host_permissions": [ 63 | "https://www.google.com/", 64 | "https://*.dropboxapi.com/*", 65 | "https://www.googleapis.com/*", 66 | "https://accounts.google.com/o/oauth2/revoke", 67 | "https://graph.microsoft.com/me/*", 68 | "https://login.microsoftonline.com/common/oauth2/v2.0/token" 69 | ], 70 | "content_security_policy": { 71 | "extension_pages": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com https://www.googleapis.com/ https://accounts.google.com/o/oauth2/revoke https://login.microsoftonline.com/common/oauth2/v2.0/token https://graph.microsoft.com/; default-src 'none'", 72 | "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-eval';" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /manifests/manifest-edge.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Authenticator: 2FA Client", 4 | "version": "8.0.2", 5 | "default_locale": "en", 6 | "description": "__MSG_extDesc__", 7 | "icons": { 8 | "16": "images/icon16.png", 9 | "48": "images/icon48.png", 10 | "128": "images/icon128.png" 11 | }, 12 | "action": { 13 | "default_icon": { 14 | "19": "images/icon19.png", 15 | "38": "images/icon38.png" 16 | }, 17 | "default_title": "__MSG_extShortName__", 18 | "default_popup": "view/popup.html" 19 | }, 20 | "commands": { 21 | "_execute_action": {}, 22 | "scan-qr": { 23 | "description": "Scan a QR code" 24 | }, 25 | "autofill": { 26 | "description": "Autofill the matched code" 27 | } 28 | }, 29 | "options_ui": { 30 | "page": "view/options.html", 31 | "open_in_tab": false 32 | }, 33 | "storage": { 34 | "managed_schema": "schema.json" 35 | }, 36 | "oauth2": { 37 | "client_id": "292457304165-u8ve4j79gag5o231n5u2pdtdrbfdo1hh.apps.googleusercontent.com", 38 | "scopes": [ 39 | "https://www.googleapis.com/auth/drive.file" 40 | ] 41 | }, 42 | "background": { 43 | "service_worker": "dist/background.js" 44 | }, 45 | "sandbox": { 46 | "pages": [ 47 | "view/argon.html" 48 | ] 49 | }, 50 | "permissions": [ 51 | "activeTab", 52 | "storage", 53 | "identity", 54 | "alarms", 55 | "scripting" 56 | ], 57 | "optional_permissions": [ 58 | "clipboardWrite", 59 | "contextMenus" 60 | ], 61 | "optional_host_permissions": [ 62 | "https://www.google.com/", 63 | "https://*.dropboxapi.com/*", 64 | "https://www.googleapis.com/*", 65 | "https://accounts.google.com/o/oauth2/revoke", 66 | "https://graph.microsoft.com/me/*", 67 | "https://login.microsoftonline.com/common/oauth2/v2.0/token" 68 | ], 69 | "content_security_policy": { 70 | "extension_pages": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com https://www.googleapis.com/ https://accounts.google.com/o/oauth2/revoke https://login.microsoftonline.com/common/oauth2/v2.0/token https://graph.microsoft.com/; default-src 'none'", 71 | "sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-eval';" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /manifests/manifest-firefox-testing.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extName__", 4 | "short_name": "__MSG_extShortName__", 5 | "version": "6.2.1", 6 | "default_locale": "en", 7 | "description": "__MSG_extDesc__", 8 | "applications": { 9 | "gecko": { 10 | "id": "authenticator@mymindstorm", 11 | "strict_min_version": "57.0" 12 | } 13 | }, 14 | "icons": { 15 | "16": "images/icon16.png", 16 | "48": "images/icon48.png", 17 | "128": "images/icon128.png" 18 | }, 19 | "browser_action": { 20 | "browser_style": false, 21 | "default_icon": { 22 | "19": "images/icon19.png", 23 | "38": "images/icon38.png" 24 | }, 25 | "default_title": "__MSG_extShortName__", 26 | "default_popup": "view/popup.html" 27 | }, 28 | "background": { 29 | "scripts": [ 30 | "dist/background.js" 31 | ] 32 | }, 33 | "commands": { 34 | "_execute_browser_action": {}, 35 | "scan-qr": { 36 | "description": "Scan a QR code" 37 | } 38 | }, 39 | "options_ui": { 40 | "page": "view/options.html", 41 | "browser_style": true 42 | }, 43 | "permissions": [ 44 | "activeTab", 45 | "", 46 | "clipboardWrite", 47 | "storage", 48 | "identity" 49 | ], 50 | "optional_permissions": [ 51 | "clipboardWrite", 52 | "https://www.google.com/", 53 | "https://*.dropboxapi.com/*", 54 | "https://www.googleapis.com/*", 55 | "https://accounts.google.com/o/oauth2/revoke", 56 | "https://graph.microsoft.com/me/*", 57 | "https://login.microsoftonline.com/common/oauth2/v2.0/token" 58 | ], 59 | "content_security_policy": "script-src 'self' 'unsafe-eval'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com https://www.googleapis.com/ https://accounts.google.com/o/oauth2/revoke https://login.microsoftonline.com/common/oauth2/v2.0/token https://graph.microsoft.com/; default-src 'none'" 60 | } 61 | -------------------------------------------------------------------------------- /manifests/manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_extName__", 4 | "short_name": "__MSG_extShortName__", 5 | "version": "8.0.2", 6 | "default_locale": "en", 7 | "description": "__MSG_extDesc__", 8 | "browser_specific_settings": { 9 | "gecko": { 10 | "id": "authenticator@mymindstorm", 11 | "strict_min_version": "126.0" 12 | } 13 | }, 14 | "icons": { 15 | "16": "images/icon16.png", 16 | "48": "images/icon48.png", 17 | "128": "images/icon128.png" 18 | }, 19 | "action": { 20 | "default_icon": { 21 | "19": "images/icon19.png", 22 | "38": "images/icon38.png" 23 | }, 24 | "default_title": "__MSG_extShortName__", 25 | "default_popup": "view/popup.html" 26 | }, 27 | "background": { 28 | "scripts": ["dist/background.js"] 29 | }, 30 | "commands": { 31 | "_execute_action": {}, 32 | "scan-qr": { 33 | "description": "Scan a QR code" 34 | }, 35 | "autofill": { 36 | "description": "Autofill the matched code" 37 | } 38 | }, 39 | "options_ui": { 40 | "page": "view/options.html", 41 | "open_in_tab": false 42 | }, 43 | "permissions": ["activeTab", "storage", "identity", "alarms", "scripting"], 44 | "optional_permissions": ["clipboardWrite"], 45 | "host_permissions": [ 46 | "https://www.google.com/", 47 | "https://*.dropboxapi.com/*", 48 | "https://www.googleapis.com/*", 49 | "https://accounts.google.com/o/oauth2/revoke", 50 | "https://graph.microsoft.com/me/*", 51 | "https://login.microsoftonline.com/common/oauth2/v2.0/token" 52 | ], 53 | "content_security_policy": { 54 | "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com https://www.googleapis.com/ https://accounts.google.com/o/oauth2/revoke https://login.microsoftonline.com/common/oauth2/v2.0/token https://graph.microsoft.com/; default-src 'none'" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /manifests/manifest-pwa.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "/", 3 | "icons": [{ 4 | "src": "/images/icon.svg", 5 | "type": "image/svg+xml", 6 | "sizes": "any", 7 | "purpose": "any maskable" 8 | }] 9 | } -------------------------------------------------------------------------------- /manifests/schema-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "disableInstallHelp": { 5 | "title": "Disable opening help page on install", 6 | "description": "If set to true, then help page will not be opened on install.", 7 | "type": "boolean" 8 | }, 9 | "disableBackup": { 10 | "title": "Disable 3rd party backup", 11 | "description": "If set to true, then 3rd party backup options will be hidden. If 3rd party backup is already configured for a user this will not stop it.", 12 | "type": "boolean" 13 | }, 14 | "disableExport": { 15 | "title": "Disable import / export menu", 16 | "description": "If set to true, then export buttons will be hidden.", 17 | "type": "boolean" 18 | }, 19 | "storageArea": { 20 | "title": "Storage area", 21 | "description": "Set to 'sync' or 'local'. If set will force user to use specified storage area. This setting will not check if a user is currently using another storage space and may hide data.", 22 | "type": "string" 23 | }, 24 | "feedbackURL": { 25 | "title": "Feedback URL", 26 | "description": "Change the URL the feedback button opens.", 27 | "type": "string" 28 | }, 29 | "enforcePassword": { 30 | "title": "Enforce password", 31 | "description": "If set to true, then user will be prompted to set a password before adding an account (if none set) and the remove password button will be hidden.", 32 | "type": "boolean" 33 | }, 34 | "enforceAutolock": { 35 | "title": "Enforce autolock", 36 | "description": "If any value is set, then the user will not be able to change the autolock setting. Set to a number in minutes.", 37 | "type": "number" 38 | }, 39 | "passwordPolicy": { 40 | "title": "Password policy", 41 | "description": "A regular expression to test if the password meets the security requirements. No slashes are needed (e.g. use [A-Z]+, but not use /[A-Z]+/).", 42 | "type": "string" 43 | }, 44 | "passwordPolicyHint": { 45 | "title": "Password policy hint", 46 | "description": "Hint to show if the password doesn't meet the security requirements.", 47 | "type": "string" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authenticator-extension", 3 | "version": "0.1.0", 4 | "description": "Authenticator generates 2-Step Verification codes in your browser.", 5 | "scripts": { 6 | "compile": "bash scripts/build.sh firefox", 7 | "dev:chrome": "npm run pretest && webpack --config ./webpack.watch.js", 8 | "chrome": "bash scripts/build.sh chrome", 9 | "firefox": "bash scripts/build.sh firefox", 10 | "edge": "bash scripts/build.sh edge", 11 | "prod": "bash scripts/build.sh prod", 12 | "pretest": "bash scripts/build.sh test", 13 | "test": "node scripts/test-runner.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/Authenticator-Extension/Authenticator.git" 18 | }, 19 | "author": "Authenticator Extension", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/Authenticator-Extension/Authenticator/issues" 23 | }, 24 | "homepage": "https://github.com/Authenticator-Extension/Authenticator#readme", 25 | "devDependencies": { 26 | "@types/argon2-browser": "^1.18.1", 27 | "@types/chai": "^4.2.14", 28 | "@types/chrome": "^0.0.266", 29 | "@types/crypto-js": "^4.1.1", 30 | "@types/mocha": "^10.0.6", 31 | "@types/sinon": "^17.0.2", 32 | "@types/sinon-chai": "^3.2.12", 33 | "@types/sinon-chrome": "^2.2.10", 34 | "@typescript-eslint/eslint-plugin": "^7.15.0", 35 | "@typescript-eslint/parser": "^7.15.0", 36 | "@vue/test-utils": "^1.1.1", 37 | "base64-loader": "^1.0.0", 38 | "buffer": "^6.0.3", 39 | "chai": "^4.2.0", 40 | "crypto-js": "^4.1.1", 41 | "eslint": "^8.56.0", 42 | "fork-ts-checker-webpack-plugin": "^6.5.3", 43 | "lodash": "^4.17.21", 44 | "mocha": "^10.2.0", 45 | "npm-license-generator": "^2.0.0", 46 | "nyc": "^15.1.0", 47 | "prettier": "2.2.1", 48 | "process": "^0.11.10", 49 | "puppeteer": "^22.11.2", 50 | "sass": "^1.26.11", 51 | "sinon": "^17.0.1", 52 | "sinon-chai": "^3.7.0", 53 | "sinon-chrome": "^3.0.1", 54 | "stream-browserify": "^3.0.0", 55 | "ts-loader": "^9.0.0", 56 | "typescript": "^5.0.0", 57 | "url-loader": "^4.0.0", 58 | "util": "^0.12.5", 59 | "vue-loader": "^15.10.1", 60 | "vue-svg-loader": "^0.16.0", 61 | "vue-template-compiler": "^2.7.16", 62 | "webpack": "^5.94.0", 63 | "webpack-cli": "^5.0.0", 64 | "webpack-merge": "^5.0.0" 65 | }, 66 | "dependencies": { 67 | "@types/lodash": "^4.14.166", 68 | "argon2-browser": "^1.18.0", 69 | "jsqr": "^1.3.1", 70 | "node-gost-crypto": "^1.0.2", 71 | "qrcode-generator": "^1.4.4", 72 | "qrcode-reader": "^1.0.4", 73 | "vue": "^2.7.16", 74 | "vue2-dragula": "^2.5.4", 75 | "vuex": "^3.4.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sass/DroidSansMono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/sass/DroidSansMono.woff2 -------------------------------------------------------------------------------- /sass/_ui.scss: -------------------------------------------------------------------------------- 1 | // Re-usable ui components 2 | 3 | // Colors 4 | // go from darkest to lightest 5 | 6 | $themes: ( 7 | normal: ( 8 | black-1: black, 9 | black-transparent: rgba(0, 0, 0, 0.5), 10 | white-1: white, 11 | white-transparent: rgba(255, 255, 255, 0.5), 12 | grey-1: grey, 13 | grey-2: #ccc, 14 | grey-3: #eee, 15 | grey-background: #eee, 16 | blue-1: #08c, 17 | yellow-1: #fff1ba, 18 | yellow-2: #fff4cc, 19 | red-1: #dd4b39, 20 | red-2: #eea59c, 21 | black-search: #2a2a2e, 22 | white-search: #f9f9fa, 23 | grey-search: #b1b1b3, 24 | blue-menu: #f4fcff, 25 | ), 26 | accessibility: ( 27 | black-1: white, 28 | black-transparent: rgba(255, 255, 255, 1), 29 | white-1: black, 30 | white-transparent: rgba(0, 0, 0, 0.5), 31 | grey-1: white, 32 | grey-2: white, 33 | grey-3: white, 34 | grey-background: black, 35 | blue-1: yellow, 36 | yellow-1: yellow, 37 | yellow-2: yellow, 38 | red-1: red, 39 | red-2: red, 40 | black-search: white, 41 | white-search: black, 42 | grey-search: white, 43 | blue-menu: cyan, 44 | ), 45 | dark: ( 46 | black-1: #ccc, 47 | black-transparent: rgba(255, 255, 255, 0.5), 48 | white-1: #242424, 49 | white-transparent: rgba(0, 0, 0, 0.5), 50 | grey-1: grey, 51 | grey-2: rgba(255, 255, 255, 0.15), 52 | grey-3: #444, 53 | grey-background: #1e1e1e, 54 | blue-1: white, 55 | yellow-1: rgba(255, 255, 255, 0.5), 56 | yellow-2: rgba(255, 255, 255, 0.35), 57 | red-1: #dd4b39, 58 | red-2: #61221a, 59 | black-search: white, 60 | white-search: #202020, 61 | grey-search: rgba(255, 255, 255, 0.35), 62 | blue-menu: #2a2d2e, 63 | ), 64 | simple: ( 65 | black-1: black, 66 | black-transparent: rgba(0, 0, 0, 0.5), 67 | white-1: white, 68 | white-transparent: rgba(255, 255, 255, 0.5), 69 | grey-1: grey, 70 | grey-2: #ccc, 71 | grey-3: #eee, 72 | grey-background: #fff, 73 | blue-1: #08c, 74 | yellow-1: #fff1ba, 75 | yellow-2: #fff4cc, 76 | red-1: #dd4b39, 77 | red-2: #eea59c, 78 | black-search: #2a2a2e, 79 | white-search: #f9f9fa, 80 | grey-search: #b1b1b3, 81 | blue-menu: #f4fcff, 82 | ), 83 | compact: ( 84 | black-1: black, 85 | black-transparent: rgba(0, 0, 0, 0.5), 86 | white-1: white, 87 | white-transparent: rgba(255, 255, 255, 0.5), 88 | grey-1: grey, 89 | grey-2: #ccc, 90 | grey-3: #eee, 91 | grey-background: #fff, 92 | blue-1: #08c, 93 | yellow-1: #fff1ba, 94 | yellow-2: #fff4cc, 95 | red-1: #dd4b39, 96 | red-2: #eea59c, 97 | black-search: #2a2a2e, 98 | white-search: #f9f9fa, 99 | grey-search: #b1b1b3, 100 | blue-menu: #f4fcff, 101 | ), 102 | flat: ( 103 | black-1: black, 104 | black-transparent: rgba(0, 0, 0, 0.5), 105 | white-1: white, 106 | white-transparent: rgba(255, 255, 255, 0.5), 107 | grey-1: grey, 108 | grey-2: #ccc, 109 | grey-3: #eee, 110 | grey-background: #eee, 111 | blue-1: #08c, 112 | yellow-1: #fff1ba, 113 | yellow-2: #fff4cc, 114 | red-1: #dd4b39, 115 | red-2: #eea59c, 116 | black-search: #2a2a2e, 117 | white-search: #f9f9fa, 118 | grey-search: #b1b1b3, 119 | blue-menu: #f4fcff, 120 | ), 121 | ); 122 | 123 | $theme-map: null; 124 | 125 | @mixin themify($themes: $themes) { 126 | @each $theme, $map in $themes { 127 | .theme-#{$theme} & { 128 | $theme-map: () !global; 129 | @each $key, $submap in $map { 130 | $value: map-get(map-get($themes, $theme), "#{$key}"); 131 | $theme-map: map-merge( 132 | $theme-map, 133 | ( 134 | $key: $value, 135 | ) 136 | ) !global; 137 | } 138 | @content; 139 | $theme-map: null !global; 140 | } 141 | } 142 | } 143 | 144 | @function themed($key) { 145 | @return map-get($theme-map, $key); 146 | } 147 | 148 | // Shared 149 | @mixin hover-black { 150 | &:hover { 151 | svg { 152 | @include themify($themes) { 153 | fill: themed("black-1"); 154 | } 155 | } 156 | } 157 | } 158 | 159 | @mixin icon-special($size, $color) { 160 | svg { 161 | vertical-align: middle; 162 | fill: $color; 163 | height: $size; 164 | width: $size; 165 | } 166 | } 167 | 168 | // Classes 169 | .button { 170 | margin: 10px; 171 | padding: 20px; 172 | border-radius: 2px; 173 | position: relative; 174 | text-align: center; 175 | font-size: 16px; 176 | width: -moz-available; 177 | width: -webkit-fill-available; 178 | @include themify($themes) { 179 | background: themed("white-1"); 180 | border: themed("grey-2") 1px solid; 181 | color: themed("grey-1"); 182 | } 183 | cursor: pointer; 184 | 185 | &:hover { 186 | @include themify($themes) { 187 | color: themed("black-1"); 188 | } 189 | } 190 | } 191 | 192 | .button-small { 193 | @extend .button; 194 | font-size: 12px; 195 | margin: 20px 100px; 196 | padding: 10px; 197 | } 198 | 199 | .input { 200 | display: block; 201 | margin: 15px 10px 20px 10px; 202 | padding: 5px 10px; 203 | width: 260px; 204 | border: none; 205 | @include themify($themes) { 206 | color: themed("black-1"); 207 | border-bottom: themed("black-1") 1px solid; 208 | background: themed("grey-3"); 209 | } 210 | outline: none; 211 | } 212 | 213 | a { 214 | @include themify($themes) { 215 | color: themed("blue-1"); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /sass/content.scss: -------------------------------------------------------------------------------- 1 | #__ga_grayLayout__ { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: rgba(255, 255, 255, 0.6); 8 | z-index: 2147483647; 9 | display: none; 10 | cursor: crosshair; 11 | } 12 | 13 | #__ga_grayLayout__ .scan { 14 | width: 100%; 15 | height: 100%; 16 | position: absolute; 17 | top: 0; 18 | opacity: 0.5; 19 | } 20 | 21 | #__ga_captureBox__ { 22 | position: absolute; 23 | border: black 1px dashed; 24 | display: none; 25 | } 26 | -------------------------------------------------------------------------------- /sass/import.scss: -------------------------------------------------------------------------------- 1 | @import "ui"; 2 | 3 | [v-cloak] { 4 | display: none; 5 | } 6 | 7 | * { 8 | font-family: arial, "Microsoft YaHei"; 9 | } 10 | 11 | p { 12 | font-size: 16px; 13 | } 14 | 15 | #import { 16 | width: 900px; 17 | position: relative; 18 | margin: 0 auto; 19 | } 20 | 21 | #import_info { 22 | margin: 10px 20px 20px 20px; 23 | } 24 | 25 | .import_tab { 26 | text-align: center; 27 | font-size: 0; 28 | 29 | input { 30 | display: none; 31 | 32 | &:checked + label { 33 | background: #eee; 34 | } 35 | } 36 | 37 | label { 38 | width: 250px; 39 | height: 50px; 40 | font-size: 18px; 41 | text-align: center; 42 | display: inline-grid; 43 | align-items: center; 44 | margin: 20px; 45 | cursor: pointer; 46 | border-radius: 2px; 47 | 48 | &:hover { 49 | background: #eee; 50 | } 51 | } 52 | } 53 | 54 | button, 55 | .import_file label { 56 | display: inline-grid; 57 | width: 260px !important; 58 | height: 60px; 59 | border: #ccc 1px solid; 60 | background: white; 61 | border-radius: 2px; 62 | position: relative; 63 | text-align: center; 64 | align-items: center; 65 | font-size: 16px; 66 | color: gray; 67 | cursor: pointer; 68 | outline: none; 69 | margin-left: 0px !important; 70 | 71 | &:hover { 72 | color: black; 73 | } 74 | } 75 | 76 | .import_file { 77 | text-align: center; 78 | 79 | input { 80 | display: none; 81 | } 82 | } 83 | 84 | .import_encrypted { 85 | margin-bottom: 20px; 86 | 87 | input { 88 | margin-left: 0; 89 | } 90 | } 91 | 92 | .import_code { 93 | float: left; 94 | margin-left: 30px; 95 | margin-right: 40px; 96 | 97 | textarea { 98 | width: 250px; 99 | height: 400px; 100 | padding: 10px; 101 | outline: none; 102 | resize: none; 103 | box-sizing: border-box; 104 | } 105 | } 106 | 107 | .error_password { 108 | font-size: 18px; 109 | color: gray; 110 | text-align: center; 111 | } 112 | 113 | .import_passphrase input, 114 | .import_file_passphrase_input input { 115 | padding: 10px; 116 | margin-bottom: 20px; 117 | width: 250px; 118 | border: #ccc 1px solid; 119 | background: white; 120 | outline: none; 121 | } 122 | 123 | .import_file_passphrase { 124 | display: grid; 125 | justify-content: center; 126 | } 127 | 128 | .import_file_passphrase_input { 129 | display: inline-grid; 130 | grid-template-rows: min-content min-content; 131 | } 132 | -------------------------------------------------------------------------------- /sass/permissions.scss: -------------------------------------------------------------------------------- 1 | @import "ui"; 2 | 3 | [v-cloak] { 4 | display: none; 5 | } 6 | 7 | * { 8 | font-family: arial, "Microsoft YaHei"; 9 | } 10 | 11 | p { 12 | font-size: 16px; 13 | } 14 | 15 | #permissions { 16 | width: 900px; 17 | position: relative; 18 | margin: 0 auto; 19 | } 20 | 21 | h2 { 22 | margin-top: 3em; 23 | } 24 | 25 | button { 26 | display: inline-grid; 27 | padding: 10px 20px; 28 | border: #ccc 1px solid; 29 | background: white; 30 | border-radius: 2px; 31 | position: relative; 32 | text-align: center; 33 | align-items: center; 34 | font-size: 16px; 35 | color: gray; 36 | cursor: pointer; 37 | outline: none; 38 | margin-left: 0px !important; 39 | 40 | &:not(:disabled):hover { 41 | color: black; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Extension build script 4 | # Syntax: 5 | # build.sh 6 | # Platforms: 7 | # 'chrome', 'firefox', 'edge', 'test', or 'prod' 8 | 9 | PLATFORM=$1 10 | REMOTE=$(git config --get remote.origin.url) 11 | CREDS=$(cat ./src/models/credentials.ts | tr -d '\n') 12 | CREDREGEX='^.*".+".*".+".*".+".*".+".*".+".*$' 13 | STYLEFILES="./src/* ./src/**/* ./src/**/**/* ./src/**/**/**/* ./sass/*.scss" 14 | set -e 15 | 16 | if [[ $PLATFORM != "chrome" ]] && [[ $PLATFORM != "firefox" ]] && [[ $PLATFORM != "edge" ]] && [[ $PLATFORM != "prod" ]] && [[ $PLATFORM != "test" ]]; then 17 | echo "Invalid platform type. Supported platforms are 'chrome', 'firefox', 'test', and 'prod'" 18 | exit 1 19 | fi 20 | 21 | echo "Removing old build files..." 22 | rm -rf build dist 23 | rm -rf firefox chrome edge release test 24 | echo "Checking style..." 25 | if ./node_modules/.bin/prettier --check $STYLEFILES 1> /dev/null ; then 26 | true 27 | else 28 | ./node_modules/.bin/prettier --check $STYLEFILES --write 29 | fi 30 | 31 | ./node_modules/.bin/eslint . --ext .js,.ts 32 | 33 | if ! [[ $CREDS =~ $CREDREGEX ]] ; then 34 | if [[ $PLATFORM = "prod" ]]; then 35 | echo -e "\e[7m\033[33mError: Missing info in credentials.ts\033[0m" 36 | exit 1 37 | else 38 | echo -e "\e[7m\033[33mWarning: Missing info in credentials.ts\033[0m" 39 | fi 40 | fi 41 | 42 | if ! [[ $REMOTE = *"https://github.com/Authenticator-Extension/Authenticator.git"* || $REMOTE = *"git@github.com:Authenticator-Extension/Authenticator.git"* || $CI ]] ; then 43 | echo 44 | echo -e "\e[7m\033[33mNotice\033[0m" 45 | echo 46 | echo -e "Thanks for forking Authenticator! If you plan on redistributing your own version of Authenticator please generate your own API keys and put them in ./src/models/credentials.ts and ./manifest-chrome.json" 47 | echo "Clear this warning by commenting it out in ./scripts/build.sh" 48 | echo 49 | read -rsp $'Press any key to continue...\n' -n1 key 50 | echo 51 | fi 52 | 53 | echo "Compiling..." 54 | if [[ $PLATFORM = "prod" ]]; then 55 | ./node_modules/webpack-cli/bin/cli.js --config webpack.prod.js 56 | elif [[ $PLATFORM = "test" ]]; then 57 | ./node_modules/webpack-cli/bin/cli.js --config webpack.dev.js 58 | ./node_modules/.bin/tsc --target ES2015 --esModuleInterop --moduleResolution nodenext --module commonjs scripts/test-runner.ts 59 | else 60 | ./node_modules/webpack-cli/bin/cli.js 61 | fi 62 | ./node_modules/sass/sass.js sass:css 63 | cp ./sass/DroidSansMono.woff2 ./sass/mocha.css ./css/ 64 | 65 | if [[ $PLATFORM = "prod" ]]; then 66 | echo "Generating licenses file..." 67 | ./node_modules/.bin/npm-license-generator \ 68 | --out-path ./view/licenses.html \ 69 | --template ./scripts/licenses-template.html \ 70 | --error-missing=true 71 | fi 72 | 73 | postCompile () { 74 | mkdir $1 75 | cp -r dist css images _locales LICENSE view $1 76 | 77 | if [[ $PLATFORM == "test" ]]; then 78 | cp manifests/manifest-$1-testing.json $1/manifest.json 79 | else 80 | cp manifests/manifest-$1.json $1/manifest.json 81 | fi 82 | 83 | if [[ $1 = "chrome" ]] || [[ $1 = "edge" ]]; then 84 | cp manifests/schema-chrome.json $1/schema.json 85 | fi 86 | 87 | cp manifests/manifest-pwa.json $1/manifest-pwa.json 88 | } 89 | 90 | if [[ $PLATFORM = "prod" ]]; then 91 | postCompile "chrome" 92 | postCompile "firefox" 93 | postCompile "edge" 94 | mkdir release 95 | mv chrome firefox edge release 96 | elif [[ $PLATFORM = "test" ]]; then 97 | postCompile "chrome" 98 | postCompile "firefox" 99 | mkdir test 100 | mv chrome firefox test 101 | else 102 | postCompile $PLATFORM 103 | fi 104 | 105 | echo -e "\033[0;32mDone!\033[0m" 106 | -------------------------------------------------------------------------------- /scripts/credentials.ts.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/scripts/credentials.ts.gpg -------------------------------------------------------------------------------- /scripts/deploy-key.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Authenticator-Extension/Authenticator/9d9660bb73700b3e725800edc22836662d94afac/scripts/deploy-key.gpg -------------------------------------------------------------------------------- /scripts/i18n.js: -------------------------------------------------------------------------------- 1 | // This checks for new stings from _locales/en/messages.json and adds them 2 | // to other translation files. Used by i18n.sh 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | 7 | function readFile(filename) { 8 | let data = fs.readFileSync(filename); 9 | data = data.toString().replace(/^\uFEFF/, ''); 10 | return JSON.parse(data); 11 | } 12 | 13 | let strings = readFile('_locales/en/messages.json'); 14 | let currentFile; 15 | let missingKeys = []; 16 | 17 | fs.readdir('_locales', function(err, items) { 18 | for (let i=0; i !currentFile.hasOwnProperty(string)); 22 | if (!missingKeys[0]) { 23 | continue; 24 | } 25 | for (let j=0; j { 28 | if (err) throw err; 29 | }) 30 | } 31 | } 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /scripts/i18n.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script automatically adds new translation strings from _locales/en/messages.json to other locale files 3 | 4 | # Define colors 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | NC='\033[0m' 8 | 9 | BRANCH=${GITHUB_REF##*/} 10 | 11 | # Configure git 12 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 13 | git config --global user.name "github-actions[bot]" 14 | git remote set-url origin git@github.com:${GITHUB_REPOSITORY}.git 15 | gpg --quiet --batch --yes --decrypt --passphrase="$DEPLOY_KEY_PASSWORD" \ 16 | --output $GITHUB_WORKSPACE/scripts/deploy-key $GITHUB_WORKSPACE/scripts/deploy-key.gpg 17 | chmod 600 $GITHUB_WORKSPACE/scripts/deploy-key 18 | eval `ssh-agent -s` &> /dev/null 19 | ssh-add $GITHUB_WORKSPACE/scripts/deploy-key &> /dev/null 20 | 21 | # Fix i18n issues 22 | cd $GITHUB_WORKSPACE 23 | node ./scripts/i18n.js 24 | 25 | # Branch changes and error with details on how to fix i18n if branched 26 | if [[ `git diff _locales` ]]; then 27 | git checkout $BRANCH &> /dev/null 28 | git add ./_locales/*/messages.json 29 | git commit -m "Add new strings" -m "This commit was automatically made by run $GITHUB_RUN_ID" --quiet 30 | git push --quiet 31 | printf "${RED}You added new strings to _locales/en/messages.json, but not some of the other translation files. A commit has been created on the current branch with the required changes already made. ${NC}\n" 32 | exit 0 33 | else 34 | printf "${GREEN}No new translation strings detected.${NC}" 35 | fi 36 | -------------------------------------------------------------------------------- /scripts/licenses-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 |

11 | 12 | Authenticator Extension 13 | 14 |

15 |
16 |

Acknowledgements

17 |

Google and Google Drive are trademarks of Google LLC.

18 |

19 | 20 | Font Awesome Free 21 | - Licensed under CC BY 4.0 22 |

23 |

24 | 25 | Droid Sans Mono 26 | - Copyright Steve Matteson. Licensed under the Apache License. 27 |

28 |

29 | Thanks to 30 | Mike Robinson 31 | <3 32 |

33 |

34 | We would like to extend our heartfelt thanks to Laurent, the Chief Information Security Officer (CISO) of the University of Luxembourg, for the invaluable support and contribution to this project. During the development process, the CISO team provided critical security recommendations that helped us identify and address potential vulnerabilities, significantly enhancing the security and reliability of the project. 35 |

36 |

37 | We especially want to acknowledge the University of Luxembourg's information security team for their selfless contribution, which not only facilitated the progress of this project but also had a positive impact on the broader open-source community. We recognize that the success of open-source software depends heavily on collaboration and support from various stakeholders, and the involvement of the University of Luxembourg has allowed us to offer a more secure and dependable product to a wider audience. 38 |

39 |

40 | We understand that while open-source software is free, maintaining and improving these projects requires significant resources. The University of Luxembourg’s information security team has demonstrated their strong commitment to the open-source community, contributing not just within their university but to users and developers globally. We hope this acknowledgment will help them continue to secure the support and resources necessary to further advance open-source initiatives. 41 |

42 |

43 | Once again, we express our sincere gratitude to the University of Luxembourg's CISO team for their valuable advice and assistance. 44 |

45 |

QR Debugging

46 | {{#renderLicenses}} 47 |

48 | {{#pkgs}} 49 | {{#homepage}}{{/homepage}}{{name}}{{#homepage}}{{/homepage}}{{#comma}}, {{/comma}} 50 | {{/pkgs}} 51 |

52 |
53 | {{text}} 54 |
55 | {{/renderLicenses}} 56 |
57 | 85 | 86 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script builds for release and puts api secrets in relevant files 3 | 4 | # Insert secrets 5 | gpg --quiet --batch --yes --decrypt --passphrase="$CREDS_FILE_PASSWORD" \ 6 | --output $GITHUB_WORKSPACE/src/models/credentials.ts $GITHUB_WORKSPACE/scripts/credentials.ts.gpg 7 | 8 | # Build release 9 | bash scripts/build.sh prod 10 | 11 | tar -cvzf release.tar.gz release/* 12 | -------------------------------------------------------------------------------- /scripts/tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used by travis to auto tag our releases 3 | 4 | # Configure git 5 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 6 | git config --global user.name "github-actions[bot]" 7 | git remote set-url origin git@github.com:${GITHUB_REPOSITORY}.git 8 | gpg --quiet --batch --yes --decrypt --passphrase="$DEPLOY_KEY_PASSWORD" \ 9 | --output $GITHUB_WORKSPACE/scripts/deploy-key $GITHUB_WORKSPACE/scripts/deploy-key.gpg 10 | chmod 600 $GITHUB_WORKSPACE/scripts/deploy-key 11 | eval `ssh-agent -s` &> /dev/null 12 | ssh-add $GITHUB_WORKSPACE/scripts/deploy-key &> /dev/null 13 | 14 | # Create and push tag 15 | export GIT_TAG=v$(grep -m 1 "\"version\"" $GITHUB_WORKSPACE/manifests/manifest-chrome.json | sed -r 's/^ *//;s/.*: *"//;s/",?//') 16 | git checkout ${GITHUB_REF##*/} 17 | git tag $GIT_TAG -a -m "Automatic tag from run $GITHUB_RUN_ID" 18 | git push origin $GIT_TAG 19 | -------------------------------------------------------------------------------- /scripts/test-runner.ts: -------------------------------------------------------------------------------- 1 | // Runs tests via puppeteer. Do not compile using webpack. 2 | 3 | import puppeteer from "puppeteer"; 4 | import path from "path"; 5 | import fs from "fs"; 6 | import { execSync } from "child_process"; 7 | import merge from "lodash/merge"; 8 | 9 | interface MochaTestResults { 10 | total?: number; 11 | tests?: StrippedTestResults[]; 12 | completed?: boolean; 13 | } 14 | 15 | interface StrippedTestResults { 16 | title: string; 17 | duration: number; 18 | path: string[]; 19 | err?: string; 20 | status: "failed" | "passed" | "pending"; 21 | } 22 | 23 | declare global { 24 | interface Window { 25 | __mocha_test_results__: MochaTestResults; 26 | } 27 | } 28 | interface TestDisplay { 29 | [key: string]: TestDisplay | StrippedTestResults 30 | } 31 | 32 | const colors = { 33 | reset: "\x1b[0m", 34 | green: "\x1b[32m", 35 | red: "\x1b[31m", 36 | } 37 | 38 | async function runTests() { 39 | const puppeteerArgs: string[] = [ 40 | `--load-extension=${path.resolve(__dirname, "../test/chrome")}`, 41 | // for CI 42 | "--no-sandbox", 43 | "--lang=en-US,en" 44 | ]; 45 | 46 | const browser = await puppeteer.launch({ 47 | ignoreDefaultArgs: ["--disable-extensions"], 48 | args: puppeteerArgs, 49 | // chrome extensions don't work in headless 50 | headless: false, 51 | executablePath: process.env.PUPPETEER_EXEC_PATH, 52 | }); 53 | const mochaPage = await browser.newPage(); 54 | await mochaPage.goto( 55 | "chrome-extension://bhghoamapcdpbohphigoooaddinpkbai/view/test.html" 56 | ); 57 | 58 | // by setting this env var, console logging works for both components and testing 59 | if (process.env.ENABLE_CONSOLE) { 60 | mochaPage.on("console", consoleMessage => console.log(consoleMessage.text())); 61 | } 62 | 63 | const results: { 64 | testResults: MochaTestResults; 65 | } = await mochaPage.evaluate(() => { 66 | return new Promise((resolve: (value: { 67 | testResults: MochaTestResults; 68 | }) => void) => { 69 | window.addEventListener("testsComplete", () => { 70 | resolve({ 71 | testResults: window.__mocha_test_results__, 72 | }); 73 | }); 74 | 75 | if (window.__mocha_test_results__.completed) { 76 | resolve({ 77 | testResults: window.__mocha_test_results__, 78 | }); 79 | } 80 | }); 81 | }); 82 | 83 | let failedTest = false; 84 | let display: TestDisplay = {}; 85 | if (results?.testResults.tests) { 86 | for (const test of results.testResults.tests) { 87 | let tmp: TestDisplay = {}; 88 | test.path.reduce((acc, current, index) => { 89 | return acc[current] = test.path.length - 1 === index ? test : {} 90 | }, tmp); 91 | display = merge(display, tmp); 92 | } 93 | } 94 | 95 | const printDisplayTests = (display: TestDisplay | StrippedTestResults) => { 96 | for (const key in display) { 97 | if (typeof display[key].status === "string") { 98 | const test = display[key]; 99 | switch (test.status) { 100 | case "passed": 101 | console.log(`${colors.green}✓${colors.reset} ${test.title}`); 102 | break; 103 | case "failed": 104 | console.log(`${colors.red}✗ ${test.title}${colors.reset}`); 105 | if (test.err) { 106 | console.log(test.err) 107 | } 108 | failedTest = true; 109 | break; 110 | case "pending": 111 | console.log(`- ${test.title}`); 112 | break; 113 | } 114 | } else { 115 | console.log(key) 116 | console.group(); 117 | printDisplayTests(display[key]); 118 | } 119 | } 120 | console.groupEnd(); 121 | } 122 | printDisplayTests(display); 123 | process.exit(failedTest ? 1 : 0); 124 | } 125 | 126 | runTests().catch(e => { 127 | console.error(e); 128 | process.exit(1); 129 | }); 130 | -------------------------------------------------------------------------------- /src/argon.ts: -------------------------------------------------------------------------------- 1 | import argon2 from "argon2-browser"; 2 | 3 | window.addEventListener("message", (event) => { 4 | const message = event.data; 5 | const source = event.source as Window; 6 | 7 | if (!source) { 8 | return; 9 | } 10 | 11 | switch (message.action) { 12 | case "hash": 13 | Argon.hash(message.value, message.salt).then((hash) => { 14 | source.postMessage({ response: hash }, event.origin); 15 | }); 16 | break; 17 | 18 | case "verify": 19 | Argon.compareHash(message.hash, message.value).then((result) => { 20 | source.postMessage({ response: result }, event.origin); 21 | }); 22 | break; 23 | 24 | default: 25 | break; 26 | } 27 | return; 28 | }); 29 | 30 | class Argon { 31 | static async hash(value: string, salt: string | Uint8Array) { 32 | const hash = await argon2.hash({ 33 | pass: value, 34 | salt: salt, 35 | time: 2, 36 | mem: 1024 * 19, 37 | parallelism: 1, 38 | hashLen: 32, 39 | type: argon2.ArgonType.Argon2id, 40 | }); 41 | 42 | return hash.encoded; 43 | } 44 | 45 | static compareHash(hash: string, value: string) { 46 | return new Promise((resolve: (value: boolean) => void) => { 47 | argon2 48 | .verify({ 49 | pass: value, 50 | encoded: hash, 51 | }) 52 | .then(() => resolve(true)) 53 | .catch((e: { message: string; code: number }) => { 54 | console.error("Error decoding hash", e); 55 | resolve(false); 56 | }); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | export const isFirefox = navigator.userAgent.indexOf("Firefox") >= 0; 2 | export const isWebKit = navigator.userAgent.indexOf("AppleWebKit") >= 0; 3 | export const isEdge = navigator.userAgent.indexOf("Edg") >= 0; 4 | export const isChromium = navigator.userAgent.indexOf("Chrome") >= 0; 5 | export const isSafari = 6 | !isChromium && navigator.userAgent.indexOf("Safari") >= 0; 7 | export const isChrome = 8 | navigator.userAgent.indexOf("Chrome") !== -1 && 9 | navigator.userAgent.indexOf("Edg") === -1; 10 | -------------------------------------------------------------------------------- /src/components/Import.vue: -------------------------------------------------------------------------------- 1 | 42 | 85 | -------------------------------------------------------------------------------- /src/components/Import/FileImport.vue: -------------------------------------------------------------------------------- 1 | 22 | 174 | -------------------------------------------------------------------------------- /src/components/Import/QrImport.vue: -------------------------------------------------------------------------------- 1 | 15 | 168 | -------------------------------------------------------------------------------- /src/components/Import/TextImport.vue: -------------------------------------------------------------------------------- 1 | 28 | 116 | -------------------------------------------------------------------------------- /src/components/Options.vue: -------------------------------------------------------------------------------- 1 | 16 | 38 | -------------------------------------------------------------------------------- /src/components/Permissions.vue: -------------------------------------------------------------------------------- 1 | 28 | 54 | -------------------------------------------------------------------------------- /src/components/Popup.vue: -------------------------------------------------------------------------------- 1 | 68 | 112 | -------------------------------------------------------------------------------- /src/components/Popup/AddAccountPage.vue: -------------------------------------------------------------------------------- 1 | 46 | 156 | -------------------------------------------------------------------------------- /src/components/Popup/AddMethodPage.vue: -------------------------------------------------------------------------------- 1 | 15 | 59 | -------------------------------------------------------------------------------- /src/components/Popup/AdvisorInsight.vue: -------------------------------------------------------------------------------- 1 | 13 | 32 | -------------------------------------------------------------------------------- /src/components/Popup/AdvisorPage.vue: -------------------------------------------------------------------------------- 1 | 20 | 46 | -------------------------------------------------------------------------------- /src/components/Popup/DrivePage.vue: -------------------------------------------------------------------------------- 1 | 32 | 136 | -------------------------------------------------------------------------------- /src/components/Popup/DropboxPage.vue: -------------------------------------------------------------------------------- 1 | 32 | 124 | -------------------------------------------------------------------------------- /src/components/Popup/EnterPasswordPage.vue: -------------------------------------------------------------------------------- 1 | 17 | 42 | -------------------------------------------------------------------------------- /src/components/Popup/LoadingPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /src/components/Popup/MenuPage.vue: -------------------------------------------------------------------------------- 1 | 74 | 170 | -------------------------------------------------------------------------------- /src/components/Popup/NotificationHandler.vue: -------------------------------------------------------------------------------- 1 | 27 | 54 | -------------------------------------------------------------------------------- /src/components/Popup/OneDrivePage.vue: -------------------------------------------------------------------------------- 1 | 42 | 129 | -------------------------------------------------------------------------------- /src/components/Popup/PageHandler.vue: -------------------------------------------------------------------------------- 1 | 13 | 57 | -------------------------------------------------------------------------------- /src/components/Popup/SetPasswordPage.vue: -------------------------------------------------------------------------------- 1 | 30 | 128 | -------------------------------------------------------------------------------- /src/components/common/ButtonInput.vue: -------------------------------------------------------------------------------- 1 | 9 | 16 | -------------------------------------------------------------------------------- /src/components/common/ButtonLink.vue: -------------------------------------------------------------------------------- 1 | 11 | 18 | -------------------------------------------------------------------------------- /src/components/common/FileInput.vue: -------------------------------------------------------------------------------- 1 | 13 | 20 | -------------------------------------------------------------------------------- /src/components/common/SelectInput.vue: -------------------------------------------------------------------------------- 1 | 15 | 22 | -------------------------------------------------------------------------------- /src/components/common/TextInput.vue: -------------------------------------------------------------------------------- 1 | 14 | 30 | -------------------------------------------------------------------------------- /src/components/common/ToggleInput.vue: -------------------------------------------------------------------------------- 1 | 12 | 26 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | import ButtonInput from "./ButtonInput.vue"; 2 | import ButtonLink from "./ButtonLink.vue"; 3 | import TextInput from "./TextInput.vue"; 4 | import SelectInput from "./SelectInput.vue"; 5 | import ToggleInput from "./ToggleInput.vue"; 6 | import FileInput from "./FileInput.vue"; 7 | 8 | export default [ 9 | { name: "a-button", component: ButtonInput }, 10 | { name: "a-button-link", component: ButtonLink }, 11 | { name: "a-text-input", component: TextInput }, 12 | { name: "a-select-input", component: SelectInput }, 13 | { name: "a-toggle-input", component: ToggleInput }, 14 | { name: "a-file-input", component: FileInput }, 15 | ]; 16 | -------------------------------------------------------------------------------- /src/definitions/BackupProvider.d.ts: -------------------------------------------------------------------------------- 1 | interface BackupProvider { 2 | upload(encryption: EncryptionInterface): Promise; 3 | getUser(): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/definitions/advisor.d.ts: -------------------------------------------------------------------------------- 1 | interface AdvisorInsightInterface { 2 | id: string; 3 | level: string; 4 | description: string; 5 | link?: string; 6 | validation: () => Promise; // true if the insight should be shown 7 | } 8 | -------------------------------------------------------------------------------- /src/definitions/gost.d.ts: -------------------------------------------------------------------------------- 1 | declare module "node-gost-crypto" { 2 | export class AlgorithmIndentifier { 3 | mode: string; 4 | name: string; 5 | version: number; 6 | length: number; 7 | } 8 | export class gostEngine { 9 | static getGostDigest(alg: AlgorithmIndentifier): GostDigest; 10 | } 11 | interface GostDigest { 12 | sign(key: Uint8Array, data: Uint8Array): number[]; 13 | verify(key: Uint8Array, signature: Uint8Array, data: Uint8Array): boolean; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/definitions/i18n.d.ts: -------------------------------------------------------------------------------- 1 | interface I18nMessage { 2 | [key: string]: { message: string; description: string }; 3 | } 4 | -------------------------------------------------------------------------------- /src/definitions/module-interface.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | interface Module { 3 | getModule(): Promise | VuexConstructor; 4 | } 5 | 6 | interface VuexConstructor { 7 | state?: { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | [key: string]: any; 10 | }; 11 | mutations?: { 12 | [key: string]: Function; 13 | }; 14 | actions?: { 15 | [key: string]: 16 | | Function 17 | | { 18 | root: boolean; 19 | handler: Function; 20 | }; 21 | }; 22 | getters?: { 23 | [key: string]: Function; 24 | }; 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | modules?: Record; 27 | plugins?: Array; 28 | strict?: boolean; 29 | devtools?: boolean; 30 | } 31 | 32 | interface MenuState { 33 | version: string; 34 | zoom: number; 35 | autolock: number; 36 | useAutofill: boolean; 37 | smartFilter: boolean; 38 | enableContextMenu: boolean; 39 | theme: string; 40 | backupDisabled: boolean; 41 | storageArea: "sync" | "local"; 42 | } 43 | 44 | interface StyleState { 45 | style: { 46 | timeout: boolean; 47 | isEditing: boolean; 48 | slidein: boolean; 49 | slideout: boolean; 50 | fadein: boolean; 51 | fadeout: boolean; 52 | show: boolean; 53 | qrfadein: boolean; 54 | qrfadeout: boolean; 55 | notificationFadein: boolean; 56 | notificationFadeout: boolean; 57 | hotpDisabled: boolean; 58 | }; 59 | } 60 | 61 | interface AccountsState { 62 | entries: OTPEntryInterface[]; 63 | defaultEncryption: string; 64 | encryption: Map; 65 | OTPType: number; 66 | shouldShowPassphrase: boolean; 67 | sectorStart: boolean; 68 | sectorOffset: number; 69 | second: number; 70 | notification: string; 71 | filter: boolean; 72 | siteName: (string | null)[]; 73 | showSearch: boolean; 74 | exportData: { [k: string]: OTPEntryInterface }; 75 | exportEncData: { [k: string]: OTPEntryInterface | Key }; 76 | keys: OldKey | Key[]; 77 | wrongPassword: boolean; 78 | initComplete: boolean; 79 | } 80 | 81 | interface NotificationState { 82 | message: Array; 83 | confirmMessage: string; 84 | messageIdle: boolean; 85 | notification: string; 86 | } 87 | 88 | interface BackupState { 89 | dropboxEncrypted: boolean; 90 | driveEncrypted: boolean; 91 | oneDriveEncrypted: boolean; 92 | dropboxToken: boolean; 93 | driveToken: boolean; 94 | oneDriveToken: boolean; 95 | } 96 | 97 | interface AdvisorState { 98 | insights: AdvisorInsightInterface[]; 99 | ignoreList: string[]; 100 | } 101 | 102 | interface PermissionsState { 103 | permissions: PermissionInterface[]; 104 | } 105 | -------------------------------------------------------------------------------- /src/definitions/otp.d.ts: -------------------------------------------------------------------------------- 1 | interface OTPEntryInterface { 2 | type: number; // OTPType 3 | index: number; 4 | issuer: string; 5 | secret: string | null; 6 | account: string; 7 | hash: string; 8 | counter: number; 9 | code: string; 10 | period: number; 11 | digits: number; 12 | algorithm: number; // OTPAlgorithm 13 | pinned: boolean; 14 | encData?: string; 15 | encryption?: EncryptionInterface; 16 | create(): Promise; 17 | update(): Promise; 18 | next(): Promise; 19 | applyEncryption(encryption: EncryptionInterface): void; 20 | changeEncryption(encryption: EncryptionInterface): void; 21 | delete(): Promise; 22 | generate(): void; 23 | genUUID(): void; 24 | } 25 | 26 | interface EncryptionInterface { 27 | getEncryptedString(data: string): string; 28 | decryptSecretString(entry: string): string | null; 29 | decryptEncSecret(entry: OTPEntryInterface): RawOTPStorage | null; 30 | getEncryptionStatus(): boolean; 31 | updateEncryptionPassword(password: string): void; 32 | getEncryptionKeyId(): string; 33 | setEncryptionKeyId(id: string): void; 34 | } 35 | 36 | interface RawOTPStorage { 37 | dataType?: "OTPStorage"; 38 | account?: string; 39 | encrypted: boolean; 40 | keyId?: string; 41 | hash: string; 42 | index: number; 43 | issuer?: string; 44 | secret: string; 45 | type: string; 46 | counter?: number; 47 | period?: number; 48 | digits?: number; 49 | algorithm?: string; 50 | pinned?: boolean; 51 | } 52 | 53 | interface EncOTPStorage { 54 | dataType: "EncOTPStorage"; 55 | keyId: string; 56 | data: string; 57 | index: number; 58 | } 59 | 60 | type OTPStorage = RawOTPStorage | EncOTPStorage; 61 | 62 | interface OldKey { 63 | enc: string; 64 | hash: string; 65 | } 66 | 67 | interface Key { 68 | dataType: "Key"; 69 | // UUID 70 | id: string; 71 | // Salt used to generate encryption key 72 | salt: string; 73 | // Hash of the encryption key 74 | hash: string; 75 | version: 3; 76 | } 77 | -------------------------------------------------------------------------------- /src/definitions/permission.d.ts: -------------------------------------------------------------------------------- 1 | interface ValidationResult { 2 | valid: boolean; 3 | message?: string; 4 | } 5 | 6 | interface PermissionInterface { 7 | id: string; 8 | description: string; 9 | revocable: boolean; 10 | validation?: Array<() => ValidationResult | Promise>; 11 | } 12 | -------------------------------------------------------------------------------- /src/definitions/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module "*.vue" { 3 | import Vue from "vue"; 4 | export default Vue; 5 | } 6 | 7 | declare module "*.svg" { 8 | import { ComponentOptions } from "vue"; 9 | const a: ComponentOptions; 10 | export default a; 11 | } 12 | -------------------------------------------------------------------------------- /src/definitions/vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Store } from "vuex"; 3 | 4 | declare module "vue/types/vue" { 5 | interface Vue { 6 | // Only in Popup 7 | $store: Store; 8 | $dragula: any; 9 | // Only in Import 10 | $entries: OTPEntryInterface[]; 11 | $encryption: EncryptionInterface; 12 | // In all 13 | i18n: { [key: string]: string }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/definitions/vue2-dragula.d.ts: -------------------------------------------------------------------------------- 1 | declare module "vue2-dragula" { 2 | import { PluginFunction, VueConstructor } from "vue"; 3 | 4 | const Vue2Dragula: PluginFunction; 5 | } 6 | -------------------------------------------------------------------------------- /src/mochaReporter.ts: -------------------------------------------------------------------------------- 1 | import { Runner, Test } from "mocha"; 2 | 3 | interface MochaTestResults { 4 | total?: number; 5 | tests?: StrippedTestResults[]; 6 | completed?: boolean; 7 | } 8 | 9 | interface StrippedTestResults { 10 | title: string; 11 | duration: number; 12 | path: string[]; 13 | err?: string; 14 | status: "failed" | "passed" | "pending"; 15 | } 16 | 17 | declare global { 18 | interface Window { 19 | __mocha_test_results__: MochaTestResults; 20 | } 21 | } 22 | 23 | export function MochaReporter(runner: Runner) { 24 | const tests: Test[] = []; 25 | 26 | runner.on("start", () => { 27 | window.__mocha_test_results__ = {}; 28 | window.__mocha_test_results__.total = runner.total; 29 | window.__mocha_test_results__.completed = false; 30 | }); 31 | 32 | runner.on("end", () => { 33 | const strip = (test: Test) => { 34 | return { 35 | title: test.title, 36 | path: test.titlePath(), 37 | duration: test.duration, 38 | err: test.err?.stack || test.err?.message, 39 | status: test.state, 40 | }; 41 | }; 42 | // @ts-expect-error typings are wrong 43 | window.__mocha_test_results__.tests = tests.map(strip); 44 | window.__mocha_test_results__.completed = true; 45 | 46 | const event = new Event("testsComplete", { bubbles: true }); 47 | window.dispatchEvent(event); 48 | }); 49 | 50 | runner.on("pending", (test: Test) => tests.push(test)); 51 | runner.on("fail", (test: Test, error: Error) => { 52 | // For some reason mocha does not put err on the test object? 53 | test.err = error; 54 | tests.push(test); 55 | }); 56 | runner.on("pass", (test: Test) => tests.push(test)); 57 | } 58 | -------------------------------------------------------------------------------- /src/models/advisor.ts: -------------------------------------------------------------------------------- 1 | export enum InsightLevel { 2 | danger = "danger", 3 | warning = "warning", 4 | info = "info", 5 | } 6 | 7 | export class AdvisorInsight implements AdvisorInsightInterface { 8 | id: string; 9 | level: string; 10 | levelText: string; 11 | description: string; 12 | link: string | undefined; 13 | validation: () => Promise; 14 | 15 | constructor(insight: AdvisorInsightInterface) { 16 | this.id = insight.id; 17 | this.level = insight.level as InsightLevel; 18 | this.levelText = chrome.i18n.getMessage(insight.level); 19 | this.description = insight.description; 20 | this.link = insight.link; 21 | this.validation = insight.validation; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/models/credentials.ts: -------------------------------------------------------------------------------- 1 | export function getCredentials() { 2 | return { 3 | drive: { 4 | client_id: "", // Google client ID 5 | client_secret: "", // Google client secret 6 | }, 7 | dropbox: { 8 | client_id: "", // Dropbox client ID 9 | }, 10 | onedrive: { 11 | client_id: "", // Microsoft Identity client ID 12 | client_secret: "", // Microsoft Identity client secret 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/models/encryption.ts: -------------------------------------------------------------------------------- 1 | import * as CryptoJS from "crypto-js"; 2 | 3 | export class Encryption implements EncryptionInterface { 4 | private password: string; 5 | private keyId: string; 6 | 7 | constructor(hash: string, keyId: string) { 8 | this.password = hash; 9 | this.keyId = keyId; 10 | } 11 | 12 | getEncryptedString(data: string): string { 13 | if (!this.password) { 14 | return data; 15 | } else { 16 | return CryptoJS.AES.encrypt(data, this.password).toString(); 17 | } 18 | } 19 | 20 | decryptSecretString(secret: string) { 21 | try { 22 | const decryptedSecret = CryptoJS.AES.decrypt( 23 | secret, 24 | this.password 25 | ).toString(CryptoJS.enc.Utf8); 26 | 27 | if (!decryptedSecret) { 28 | return null; 29 | } 30 | 31 | if (decryptedSecret.length < 8) { 32 | return null; 33 | } 34 | 35 | if ( 36 | !/^[a-z2-7]+=*$/i.test(decryptedSecret) && 37 | !/^[0-9a-f]+$/i.test(decryptedSecret) && 38 | !/^blz-/.test(decryptedSecret) && 39 | !/^bliz-/.test(decryptedSecret) && 40 | !/^stm-/.test(decryptedSecret) 41 | ) { 42 | return null; 43 | } 44 | 45 | return decryptedSecret; 46 | } catch (error) { 47 | return null; 48 | } 49 | } 50 | 51 | decryptEncSecret(entry: OTPEntryInterface) { 52 | try { 53 | if (!entry.encData) { 54 | return null; 55 | } 56 | 57 | const decryptedData = CryptoJS.AES.decrypt( 58 | entry.encData, 59 | this.password 60 | ).toString(CryptoJS.enc.Utf8); 61 | 62 | if (!decryptedData) { 63 | return null; 64 | } 65 | 66 | return JSON.parse(decryptedData); 67 | } catch (error) { 68 | return null; 69 | } 70 | } 71 | 72 | getEncryptionStatus(): boolean { 73 | return this.password ? true : false; 74 | } 75 | 76 | updateEncryptionPassword(password: string) { 77 | this.password = password; 78 | } 79 | 80 | setEncryptionKeyId(id: string): void { 81 | this.keyId = id; 82 | } 83 | 84 | getEncryptionKeyId(): string { 85 | return this.keyId; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/models/migration.ts: -------------------------------------------------------------------------------- 1 | import * as CryptoJS from "crypto-js"; 2 | 3 | function byteArray2Base32(bytes: number[]) { 4 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 5 | const len = bytes.length; 6 | let result = ""; 7 | let high = 0, 8 | low = 0, 9 | sh = 0, 10 | hasDataInLow = false; 11 | for (let i = 0; i < len; i += 5) { 12 | hasDataInLow = true; 13 | high = 0xf8 & bytes[i]; 14 | result += chars.charAt(high >> 3); 15 | low = 0x07 & bytes[i]; 16 | sh = 2; 17 | 18 | if (i + 1 < len) { 19 | high = 0xc0 & bytes[i + 1]; 20 | result += chars.charAt((low << 2) + (high >> 6)); 21 | result += chars.charAt((0x3e & bytes[i + 1]) >> 1); 22 | low = bytes[i + 1] & 0x01; 23 | sh = 4; 24 | } 25 | 26 | if (i + 2 < len) { 27 | high = 0xf0 & bytes[i + 2]; 28 | result += chars.charAt((low << 4) + (high >> 4)); 29 | low = 0x0f & bytes[i + 2]; 30 | sh = 1; 31 | } 32 | 33 | if (i + 3 < len) { 34 | high = 0x80 & bytes[i + 3]; 35 | result += chars.charAt((low << 1) + (high >> 7)); 36 | result += chars.charAt((0x7c & bytes[i + 3]) >> 2); 37 | low = 0x03 & bytes[i + 3]; 38 | sh = 3; 39 | } 40 | 41 | if (i + 4 < len) { 42 | hasDataInLow = false; 43 | high = 0xe0 & bytes[i + 4]; 44 | result += chars.charAt((low << 3) + (high >> 5)); 45 | result += chars.charAt(0x1f & bytes[i + 4]); 46 | low = 0; 47 | sh = 0; 48 | } 49 | } 50 | 51 | if (hasDataInLow) { 52 | result += chars.charAt(low << sh); 53 | } 54 | 55 | const padlen = 8 - (result.length % 8); 56 | return result + (padlen < 8 ? Array(padlen + 1).join("=") : ""); 57 | } 58 | 59 | function wordArrayToByteArray(wordArray: CryptoJS.lib.WordArray) { 60 | const byteArray: number[] = []; 61 | for (let i = 0; i < wordArray.words.length; ++i) { 62 | const word = wordArray.words[i]; 63 | for (let j = 3; j >= 0; --j) { 64 | byteArray.push((word >> (8 * j)) & 0xff); 65 | } 66 | } 67 | byteArray.length = wordArray.sigBytes; 68 | return byteArray; 69 | } 70 | 71 | function byteArray2String(bytes: number[]) { 72 | return String.fromCharCode.apply(null, bytes); 73 | } 74 | 75 | function subBytesArray(bytes: number[], start: number, length: number) { 76 | const subBytes: number[] = []; 77 | for (let i = 0; i < length; i++) { 78 | subBytes.push(bytes[start + i]); 79 | } 80 | return subBytes; 81 | } 82 | 83 | export function getOTPAuthPerLineFromOPTAuthMigration(migrationUri: string) { 84 | if (!migrationUri.startsWith("otpauth-migration:")) { 85 | return []; 86 | } 87 | 88 | const base64Data = decodeURIComponent(migrationUri.split("data=")[1]); 89 | const wordArrayData = CryptoJS.enc.Base64.parse(base64Data); 90 | const byteData = wordArrayToByteArray(wordArrayData); 91 | const lines: string[] = []; 92 | let offset = 0; 93 | while (offset < byteData.length) { 94 | if (byteData[offset] !== 10) { 95 | break; 96 | } 97 | const lineLength = byteData[offset + 1]; 98 | const secretStart = offset + 4; 99 | const secretLength = byteData[offset + 3]; 100 | const secretBytes = subBytesArray(byteData, secretStart, secretLength); 101 | const secret = byteArray2Base32(secretBytes); 102 | const accountStart = secretStart + secretLength + 2; 103 | const accountLength = byteData[secretStart + secretLength + 1]; 104 | const accountBytes = subBytesArray(byteData, accountStart, accountLength); 105 | const account = byteArray2String(accountBytes); 106 | const isserStart = accountStart + accountLength + 2; 107 | const isserLength = byteData[accountStart + accountLength + 1]; 108 | const issuerBytes = subBytesArray(byteData, isserStart, isserLength); 109 | const issuer = byteArray2String(issuerBytes); 110 | const algorithm = ["SHA1", "SHA1", "SHA256", "SHA512", "MD5"][ 111 | byteData[isserStart + isserLength + 1] 112 | ]; 113 | const digits = [6, 6, 8][byteData[isserStart + isserLength + 3]]; 114 | const type = ["totp", "hotp", "totp"][ 115 | byteData[isserStart + isserLength + 5] 116 | ]; 117 | let line = `otpauth://${type}/${account}?secret=${secret}&issuer=${issuer}&algorithm=${algorithm}&digits=${digits}`; 118 | if (type === "hotp") { 119 | let counter = 1; 120 | if (isserStart + isserLength + 7 <= lineLength) { 121 | counter = byteData[isserStart + isserLength + 7]; 122 | } 123 | line += `&counter=${counter}`; 124 | } 125 | lines.push(line); 126 | offset += lineLength + 2; 127 | } 128 | return lines; 129 | } 130 | -------------------------------------------------------------------------------- /src/models/password.ts: -------------------------------------------------------------------------------- 1 | import { BrowserStorage, isOldKey } from "./storage"; 2 | 3 | export async function argonHash( 4 | value: string, 5 | salt: string 6 | ): Promise { 7 | const iframe = document.getElementById("argon-sandbox"); 8 | const message = { 9 | action: "hash", 10 | value, 11 | salt, 12 | }; 13 | 14 | if (!iframe) { 15 | throw new Error("argon-sandbox missing!"); 16 | } 17 | 18 | const argonPromise: Promise = new Promise((resolve) => { 19 | window.addEventListener("message", (response) => { 20 | resolve(response.data.response); 21 | }); 22 | // @ts-expect-error bad typings 23 | iframe.contentWindow.postMessage(message, "*"); 24 | }); 25 | 26 | return argonPromise; 27 | } 28 | 29 | export async function argonVerify( 30 | value: string, 31 | hash: string 32 | ): Promise { 33 | const iframe = document.getElementById("argon-sandbox"); 34 | const message = { 35 | action: "verify", 36 | value, 37 | hash, 38 | }; 39 | 40 | if (!iframe) { 41 | throw new Error("argon-sandbox missing!"); 42 | } 43 | 44 | const argonPromise: Promise = new Promise((resolve) => { 45 | window.addEventListener("message", (response) => { 46 | resolve(response.data.response); 47 | }); 48 | // @ts-expect-error bad typings 49 | iframe.contentWindow.postMessage(message, "*"); 50 | }); 51 | 52 | return argonPromise; 53 | } 54 | 55 | // Verify a password using keys in BrowserStorage 56 | export async function verifyPasswordUsingKeyID( 57 | keyId: string, 58 | password: string 59 | ): Promise { 60 | // Get key for current encryption 61 | const keys = await BrowserStorage.getKeys(); 62 | if (isOldKey(keys)) { 63 | throw new Error( 64 | "v3 encryption not being used with verifyPassword. This should never happen!" 65 | ); 66 | } 67 | 68 | const key = keys.find((key) => key.id === keyId); 69 | if (!key) { 70 | throw new Error(`Key ${keyId} not in BrowserStorage`); 71 | } 72 | 73 | return verifyPasswordUsingKey(key, password); 74 | } 75 | 76 | export async function verifyPasswordUsingKey( 77 | key: Key, 78 | password: string 79 | ): Promise { 80 | // Hash password with argon 81 | const rawHash = await argonHash(password, key.salt); 82 | if (!rawHash) { 83 | throw new Error("argon2 did not return a hash!"); 84 | } 85 | // https://passlib.readthedocs.io/en/stable/lib/passlib.hash.argon2.html#format-algorithm 86 | const possibleHash = rawHash.split("$")[5]; 87 | 88 | // verify user password by comparing their password hash with the 89 | // hash of their password's hash 90 | return await argonVerify(possibleHash, key.hash); 91 | } 92 | -------------------------------------------------------------------------------- /src/models/permission.ts: -------------------------------------------------------------------------------- 1 | export class Permission implements PermissionInterface { 2 | id: string; 3 | description: string; 4 | revocable: boolean; 5 | validation?: Array<() => ValidationResult | Promise>; 6 | 7 | constructor(permission: PermissionInterface) { 8 | this.id = permission.id; 9 | this.description = permission.description; 10 | this.revocable = permission.revocable; 11 | this.validation = permission.validation; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import OptionsView from "./components/Options.vue"; 3 | import { loadI18nMessages } from "./store/i18n"; 4 | 5 | async function init() { 6 | // i18n 7 | Vue.prototype.i18n = await loadI18nMessages(); 8 | 9 | new Vue({ 10 | render: (h) => h(OptionsView), 11 | }).$mount("#options"); 12 | } 13 | 14 | init(); 15 | -------------------------------------------------------------------------------- /src/permissions.ts: -------------------------------------------------------------------------------- 1 | // Vue 2 | import Vue from "vue"; 3 | import Vuex from "vuex"; 4 | 5 | // Components 6 | import PermissionsView from "./components/Permissions.vue"; 7 | import CommonComponents from "./components/common/index"; 8 | 9 | // Other 10 | import { loadI18nMessages } from "./store/i18n"; 11 | import { Permissions } from "./store/Permissions"; 12 | 13 | async function init() { 14 | // i18n 15 | Vue.prototype.i18n = await loadI18nMessages(); 16 | 17 | // Load modules 18 | Vue.use(Vuex); 19 | 20 | // Load common components globally 21 | for (const component of CommonComponents) { 22 | Vue.component(component.name, component.component); 23 | } 24 | 25 | // State 26 | const store = new Vuex.Store({ 27 | modules: { 28 | permissions: await new Permissions().getModule(), 29 | }, 30 | }); 31 | 32 | const instance = new Vue({ 33 | render: (h) => h(PermissionsView), 34 | store, 35 | }).$mount("#permissions"); 36 | 37 | // Set title 38 | try { 39 | document.title = instance.i18n.extName; 40 | } catch (e) { 41 | console.error(e); 42 | } 43 | } 44 | 45 | init(); 46 | -------------------------------------------------------------------------------- /src/qrdebug.ts: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((message, sender) => { 2 | if (message.action === "position") { 3 | if (!sender.tab) { 4 | return; 5 | } 6 | getQrDebug( 7 | sender.tab, 8 | message.info.left, 9 | message.info.top, 10 | message.info.width, 11 | message.info.height, 12 | message.info.windowWidth 13 | ); 14 | } 15 | 16 | // https://stackoverflow.com/a/56483156 17 | return true; 18 | }); 19 | 20 | function getQrDebug( 21 | tab: chrome.tabs.Tab, 22 | left: number, 23 | top: number, 24 | width: number, 25 | height: number, 26 | windowWidth: number 27 | ) { 28 | chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" }, (dataUrl) => { 29 | const qr = new Image(); 30 | qr.src = dataUrl; 31 | qr.onload = () => { 32 | const devicePixelRatio = qr.width / windowWidth; 33 | const captureCanvas = document.createElement("canvas"); 34 | captureCanvas.width = width * devicePixelRatio; 35 | captureCanvas.height = height * devicePixelRatio; 36 | const ctx = captureCanvas.getContext("2d"); 37 | if (!ctx) { 38 | return; 39 | } 40 | ctx.drawImage( 41 | qr, 42 | left * devicePixelRatio, 43 | top * devicePixelRatio, 44 | width * devicePixelRatio, 45 | height * devicePixelRatio, 46 | 0, 47 | 0, 48 | width * devicePixelRatio, 49 | height * devicePixelRatio 50 | ); 51 | const url = captureCanvas.toDataURL(); 52 | const infoDom = document.getElementById("info"); 53 | if (infoDom) { 54 | infoDom.innerHTML = 55 | "Scan Data:
" + 56 | `
` + 57 | `Window Inner Width: ${windowWidth}
` + 58 | `Width: ${width}
` + 59 | `Height: ${height}
` + 60 | `Left: ${left}
` + 61 | `Top: ${top}
` + 62 | `Screen Width: ${window.screen.width}
` + 63 | `Screen Height: ${window.screen.height}
` + 64 | `Capture Width: ${qr.width}
` + 65 | `Capture Height: ${qr.height}
` + 66 | `Device Pixel Ratio: ${devicePixelRatio} / ${window.devicePixelRatio}
` + 67 | `Tab ID: ${tab.id}
` + 68 | "
" + 69 | "Captured Screenshot:"; 70 | } 71 | 72 | const qrDom = document.getElementById("qr") as HTMLImageElement; 73 | if (qrDom) { 74 | qrDom.src = url; 75 | } 76 | }; 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/store/Advisor.ts: -------------------------------------------------------------------------------- 1 | import { EntryStorage } from "../models/storage"; 2 | import { InsightLevel, AdvisorInsight } from "../models/advisor"; 3 | import { StorageLocation, UserSettings } from "../models/settings"; 4 | 5 | const insightsData: AdvisorInsightInterface[] = [ 6 | { 7 | id: "passwordNotSet", 8 | level: InsightLevel.danger, 9 | description: chrome.i18n.getMessage("advisor_insight_password_not_set"), 10 | validation: async () => { 11 | const hasEncryptedEntry = await EntryStorage.hasEncryptionKey(); 12 | return !hasEncryptedEntry; 13 | }, 14 | }, 15 | { 16 | id: "autoLockNotSet", 17 | level: InsightLevel.warning, 18 | description: chrome.i18n.getMessage("advisor_insight_auto_lock_not_set"), 19 | validation: async () => { 20 | await UserSettings.updateItems(); 21 | const hasEncryptedEntry = await EntryStorage.hasEncryptionKey(); 22 | return hasEncryptedEntry && !Number(UserSettings.items.autolock); 23 | }, 24 | }, 25 | { 26 | id: "browserSyncNotEnabled", 27 | level: InsightLevel.info, 28 | description: chrome.i18n.getMessage( 29 | "advisor_insight_browser_sync_not_enabled" 30 | ), 31 | validation: async () => { 32 | await UserSettings.updateItems(); 33 | const storageArea = UserSettings.items.storageLocation; 34 | return storageArea !== StorageLocation.Sync; 35 | }, 36 | }, 37 | { 38 | id: "autoFillNotEnabled", 39 | level: InsightLevel.info, 40 | description: chrome.i18n.getMessage( 41 | "advisor_insight_auto_fill_not_enabled" 42 | ), 43 | validation: async () => { 44 | await UserSettings.updateItems(); 45 | return UserSettings.items.autofill !== true; 46 | }, 47 | }, 48 | { 49 | id: "smartFilterNotEnabled", 50 | level: InsightLevel.info, 51 | description: chrome.i18n.getMessage( 52 | "advisor_insight_smart_filter_not_enabled" 53 | ), 54 | validation: async () => { 55 | await UserSettings.updateItems(); 56 | return UserSettings.items.smartFilter === false; 57 | }, 58 | }, 59 | ]; 60 | 61 | export class Advisor implements Module { 62 | async getModule() { 63 | await UserSettings.updateItems(); 64 | return { 65 | state: { 66 | insights: await this.getInsights(), 67 | ignoreList: UserSettings.items.advisorIgnoreList || [], 68 | }, 69 | mutations: { 70 | dismissInsight: async (state: AdvisorState, insightId: string) => { 71 | state.ignoreList.push(insightId); 72 | UserSettings.items.advisorIgnoreList = state.ignoreList; 73 | UserSettings.commitItems(); 74 | 75 | state.insights = await this.getInsights(); 76 | }, 77 | clearIgnoreList: async (state: AdvisorState) => { 78 | state.ignoreList = []; 79 | UserSettings.items.advisorIgnoreList = undefined; 80 | UserSettings.commitItems(); 81 | 82 | state.insights = await this.getInsights(); 83 | }, 84 | updateInsight: async (state: AdvisorState) => { 85 | state.insights = await this.getInsights(); 86 | state.ignoreList = 87 | typeof UserSettings.items.advisorIgnoreList === "string" 88 | ? JSON.parse(UserSettings.items.advisorIgnoreList || "[]") 89 | : UserSettings.items.advisorIgnoreList || []; 90 | }, 91 | }, 92 | namespaced: true, 93 | }; 94 | } 95 | 96 | private async getInsights() { 97 | await UserSettings.updateItems(); 98 | const advisorIgnoreList: string[] = 99 | typeof UserSettings.items.advisorIgnoreList === "string" 100 | ? JSON.parse(UserSettings.items.advisorIgnoreList || "[]") 101 | : UserSettings.items.advisorIgnoreList || []; 102 | 103 | const filteredInsightsData: AdvisorInsightInterface[] = []; 104 | 105 | for (const insightData of insightsData) { 106 | if (advisorIgnoreList.includes(insightData.id)) { 107 | continue; 108 | } 109 | 110 | const validation = await insightData.validation(); 111 | 112 | if (validation) { 113 | filteredInsightsData.push(insightData); 114 | } 115 | } 116 | 117 | return filteredInsightsData.map( 118 | (insightData) => new AdvisorInsight(insightData) 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/store/Backup.ts: -------------------------------------------------------------------------------- 1 | import { UserSettings } from "../models/settings"; 2 | 3 | export class Backup implements Module { 4 | async getModule() { 5 | UserSettings.updateItems(); 6 | 7 | return { 8 | state: { 9 | dropboxEncrypted: UserSettings.items.dropboxEncrypted === true, 10 | driveEncrypted: UserSettings.items.driveEncrypted === true, 11 | oneDriveEncrypted: UserSettings.items.oneDriveEncrypted === true, 12 | dropboxToken: Boolean(UserSettings.items.dropboxToken), 13 | driveToken: Boolean(UserSettings.items.driveToken), 14 | oneDriveToken: Boolean(UserSettings.items.oneDriveToken), 15 | }, 16 | mutations: { 17 | setToken( 18 | state: BackupState, 19 | args: { service: string; value: boolean } 20 | ) { 21 | switch (args.service) { 22 | case "dropbox": 23 | state.dropboxToken = args.value; 24 | break; 25 | 26 | case "drive": 27 | state.driveToken = args.value; 28 | break; 29 | 30 | case "onedrive": 31 | state.oneDriveToken = args.value; 32 | break; 33 | 34 | default: 35 | break; 36 | } 37 | }, 38 | setEnc(state: BackupState, args: { service: string; value: boolean }) { 39 | switch (args.service) { 40 | case "dropbox": 41 | state.dropboxEncrypted = args.value; 42 | break; 43 | 44 | case "drive": 45 | state.driveEncrypted = args.value; 46 | break; 47 | 48 | case "onedrive": 49 | state.oneDriveEncrypted = args.value; 50 | break; 51 | 52 | default: 53 | break; 54 | } 55 | }, 56 | }, 57 | namespaced: true, 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/store/CurrentView.ts: -------------------------------------------------------------------------------- 1 | export class CurrentView implements Module { 2 | getModule() { 3 | return { 4 | state: { 5 | info: "", 6 | }, 7 | mutations: { 8 | changeView(state: { info: string }, viewName: string) { 9 | state.info = viewName; 10 | }, 11 | }, 12 | namespaced: true, 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/store/Menu.ts: -------------------------------------------------------------------------------- 1 | import { isSafari } from "../browser"; 2 | import { UserSettings } from "../models/settings"; 3 | import { ManagedStorage } from "../models/storage"; 4 | 5 | export class Menu implements Module { 6 | async getModule() { 7 | await UserSettings.updateItems(); 8 | 9 | const menuState = { 10 | state: { 11 | version: chrome.runtime.getManifest()?.version || "0.0.0", 12 | zoom: Number(UserSettings.items.zoom) || 100, 13 | useAutofill: UserSettings.items.autofill === true, 14 | smartFilter: UserSettings.items.smartFilter === true, 15 | enableContextMenu: UserSettings.items.enableContextMenu === true, 16 | theme: UserSettings.items.theme || (isSafari ? "flat" : "normal"), 17 | autolock: Number(UserSettings.items.autolock) || 30, 18 | backupDisabled: await ManagedStorage.get("disableBackup", false), 19 | exportDisabled: await ManagedStorage.get("disableExport", false), 20 | enforcePassword: await ManagedStorage.get("enforcePassword", false), 21 | enforceAutolock: await ManagedStorage.get("enforceAutolock", false), 22 | storageArea: await ManagedStorage.get<"sync" | "local">("storageArea"), 23 | feedbackURL: await ManagedStorage.get("feedbackURL"), 24 | passwordPolicy: await ManagedStorage.get("passwordPolicy"), 25 | passwordPolicyHint: await ManagedStorage.get( 26 | "passwordPolicyHint" 27 | ), 28 | }, 29 | mutations: { 30 | setZoom: (state: MenuState, zoom: number) => { 31 | state.zoom = zoom; 32 | UserSettings.items.zoom = zoom; 33 | UserSettings.commitItems(); 34 | this.resize(zoom); 35 | }, 36 | setAutofill(state: MenuState, useAutofill: boolean) { 37 | state.useAutofill = useAutofill; 38 | UserSettings.items.autofill = useAutofill; 39 | UserSettings.commitItems(); 40 | }, 41 | setSmartFilter(state: MenuState, smartFilter: boolean) { 42 | state.smartFilter = smartFilter; 43 | UserSettings.items.smartFilter = smartFilter; 44 | UserSettings.commitItems(); 45 | }, 46 | setEnableContextMenu(state: MenuState, enableContextMenu: boolean) { 47 | state.enableContextMenu = enableContextMenu; 48 | UserSettings.items.enableContextMenu = enableContextMenu; 49 | UserSettings.commitItems(); 50 | }, 51 | setTheme(state: MenuState, theme: string) { 52 | state.theme = theme; 53 | UserSettings.items.theme = theme; 54 | UserSettings.commitItems(); 55 | }, 56 | setAutolock(state: MenuState, autolock: number) { 57 | state.autolock = autolock; 58 | UserSettings.items.autolock = autolock; 59 | UserSettings.commitItems(); 60 | }, 61 | }, 62 | namespaced: true, 63 | }; 64 | 65 | this.resize(menuState.state.zoom); 66 | 67 | return menuState; 68 | } 69 | 70 | private resize(zoom: number) { 71 | if (zoom !== 100) { 72 | document.body.style.marginBottom = 480 * (zoom / 100 - 1) + "px"; 73 | document.body.style.marginRight = 320 * (zoom / 100 - 1) + "px"; 74 | document.body.style.transform = "scale(" + zoom / 100 + ")"; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/store/Notification.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext } from "vuex"; 2 | 3 | export class Notification implements Module { 4 | getModule() { 5 | return { 6 | state: { 7 | message: [], // Message content for alert with ok button 8 | confirmMessage: "", // Message content for alert with yes / no 9 | messageIdle: true, // Should show alert box? 10 | notification: "", // Ephermal message text 11 | }, 12 | mutations: { 13 | alert: (state: NotificationState, message: string) => { 14 | state.message.unshift(message); 15 | }, 16 | closeAlert: (state: NotificationState) => { 17 | state.messageIdle = false; 18 | state.message.shift(); 19 | setTimeout(() => { 20 | state.messageIdle = true; 21 | }, 200); 22 | }, 23 | setConfirm: (state: NotificationState, message: string) => { 24 | state.confirmMessage = message; 25 | }, 26 | setNotification: (state: NotificationState, message: string) => { 27 | state.notification = message; 28 | }, 29 | }, 30 | actions: { 31 | confirm: async ( 32 | state: ActionContext, 33 | message: string 34 | ) => { 35 | return new Promise((resolve: (value: boolean) => void) => { 36 | state.commit("setConfirm", message); 37 | window.addEventListener("confirm", (event) => { 38 | state.commit("setConfirm", ""); 39 | if (!this.isCustomEvent(event)) { 40 | resolve(false); 41 | return; 42 | } 43 | resolve(event.detail); 44 | return; 45 | }); 46 | }); 47 | }, 48 | ephermalMessage: ( 49 | state: ActionContext, 50 | message: string 51 | ) => { 52 | state.commit("setNotification", message); 53 | state.commit("style/showNotification", null, { root: true }); 54 | }, 55 | }, 56 | namespaced: true, 57 | }; 58 | } 59 | 60 | private isCustomEvent(event: Event): event is CustomEvent { 61 | return "detail" in event; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/store/Qr.ts: -------------------------------------------------------------------------------- 1 | export class Qr implements Module { 2 | getModule() { 3 | return { 4 | state: { 5 | qr: "", 6 | }, 7 | mutations: { 8 | setQr(state: { qr: string }, url: string) { 9 | state.qr = `url(${url})`; 10 | }, 11 | }, 12 | namespaced: true, 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/store/Style.ts: -------------------------------------------------------------------------------- 1 | export class Style implements Module { 2 | getModule() { 3 | return { 4 | state: { 5 | style: { 6 | timeout: false, 7 | isEditing: false, 8 | slidein: false, // menu 9 | slideout: false, // menu 10 | fadein: false, // info 11 | fadeout: false, // info 12 | show: false, // info 13 | qrfadein: false, 14 | qrfadeout: false, 15 | notificationFadein: false, 16 | notificationFadeout: false, 17 | hotpDisabled: false, 18 | }, 19 | }, 20 | mutations: { 21 | showMenu(state: StyleState) { 22 | state.style.slidein = true; 23 | state.style.slideout = false; 24 | }, 25 | hideMenu(state: StyleState) { 26 | state.style.slidein = false; 27 | state.style.slideout = true; 28 | setTimeout(() => { 29 | state.style.slideout = false; 30 | }, 200); 31 | }, 32 | showInfo(state: StyleState, noAnimate?: boolean) { 33 | if (noAnimate) { 34 | state.style.show = true; 35 | } else { 36 | state.style.fadein = true; 37 | state.style.fadeout = false; 38 | } 39 | }, 40 | hideInfo(state: StyleState, noAnimate?: boolean) { 41 | if (noAnimate) { 42 | state.style.show = false; 43 | } else { 44 | state.style.fadein = false; 45 | state.style.fadeout = true; 46 | } 47 | setTimeout(() => { 48 | state.style.fadeout = false; 49 | }, 200); 50 | }, 51 | showQr(state: StyleState) { 52 | state.style.qrfadein = true; 53 | state.style.qrfadeout = false; 54 | }, 55 | hideQr(state: StyleState) { 56 | state.style.qrfadein = false; 57 | state.style.qrfadeout = true; 58 | setTimeout(() => { 59 | state.style.qrfadeout = false; 60 | }, 200); 61 | }, 62 | showNotification(state: StyleState) { 63 | state.style.notificationFadein = true; 64 | state.style.notificationFadeout = false; 65 | setTimeout(() => { 66 | state.style.notificationFadein = false; 67 | state.style.notificationFadeout = true; 68 | setTimeout(() => { 69 | state.style.notificationFadeout = false; 70 | }, 200); 71 | }, 1000); 72 | }, 73 | toggleEdit(state: StyleState) { 74 | state.style.isEditing = !state.style.isEditing; 75 | }, 76 | toggleHotpDisabled(state: StyleState) { 77 | state.style.hotpDisabled = !state.style.hotpDisabled; 78 | }, 79 | }, 80 | getters: { 81 | // Returns true if menu or info screen shown 82 | isMenuShown(state: StyleState) { 83 | return state.style.fadein || state.style.show || state.style.slidein; 84 | }, 85 | }, 86 | namespaced: true, 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/store/i18n.ts: -------------------------------------------------------------------------------- 1 | export async function loadI18nMessages() { 2 | return new Promise( 3 | ( 4 | resolve: (value: { [key: string]: string }) => void, 5 | reject: (reason: Error) => void 6 | ) => { 7 | try { 8 | const xhr = new XMLHttpRequest(); 9 | xhr.overrideMimeType("application/json"); 10 | xhr.onreadystatechange = () => { 11 | if (xhr.readyState === 4) { 12 | const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); 13 | const i18nData: { [key: string]: string } = {}; 14 | for (const key of Object.keys(i18nMessage)) { 15 | i18nData[key] = chrome.i18n.getMessage(key); 16 | } 17 | return resolve(i18nData); 18 | } 19 | return; 20 | }; 21 | xhr.open("GET", chrome.runtime.getURL("/_locales/en/messages.json")); 22 | xhr.send(); 23 | } catch (error) { 24 | if (typeof error === "string" || error === undefined) { 25 | return reject(Error(error)); 26 | } else if (error instanceof Error) { 27 | return reject(error); 28 | } else { 29 | return reject(Error(String(error))); 30 | } 31 | } 32 | } 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/syncTime.ts: -------------------------------------------------------------------------------- 1 | import { UserSettings } from "./models/settings"; 2 | 3 | export async function syncTimeWithGoogle() { 4 | await UserSettings.updateItems(); 5 | 6 | return new Promise( 7 | (resolve: (value: string) => void, reject: (reason: Error) => void) => { 8 | try { 9 | // @ts-expect-error - these typings are wrong 10 | const xhr = new XMLHttpRequest({ mozAnon: true }); 11 | xhr.open("HEAD", "https://www.google.com/generate_204"); 12 | const xhrAbort = setTimeout(() => { 13 | xhr.abort(); 14 | return resolve("updateFailure"); 15 | }, 5000); 16 | xhr.onreadystatechange = () => { 17 | if (xhr.readyState === 4) { 18 | clearTimeout(xhrAbort); 19 | const date = xhr.getResponseHeader("date"); 20 | if (!date) { 21 | return resolve("updateFailure"); 22 | } 23 | const serverTime = new Date(date).getTime(); 24 | const clientTime = new Date().getTime(); 25 | const offset = Math.round((serverTime - clientTime) / 1000); 26 | 27 | if (Math.abs(offset) <= 300) { 28 | // within 5 minutes 29 | UserSettings.items.offset = Math.round( 30 | (serverTime - clientTime) / 1000 31 | ); 32 | UserSettings.commitItems(); 33 | return resolve("updateSuccess"); 34 | } else { 35 | return resolve("clock_too_far_off"); 36 | } 37 | } 38 | }; 39 | xhr.send(); 40 | } catch (error) { 41 | return reject(error as Error); 42 | } 43 | } 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | import { MochaReporter } from "./mochaReporter"; 3 | import sinon from "sinon"; 4 | 5 | // @ts-expect-error this is not a node require 6 | const tests = require.context("./test", true, /\.tsx?$/); 7 | tests.keys().forEach(tests); 8 | 9 | mocha.setup({ 10 | // @ts-expect-error - typings are wrong 11 | reporter: MochaReporter, 12 | rootHooks: { 13 | afterEach() { 14 | sinon.restore(); 15 | }, 16 | }, 17 | }); 18 | 19 | mocha.run(); 20 | -------------------------------------------------------------------------------- /src/test/components/Popup/EnterPasswordPage.test.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | import * as chai from "chai"; 3 | import * as sinon from "sinon"; 4 | import * as sinonChai from "sinon-chai"; 5 | 6 | import { mount, createLocalVue } from "@vue/test-utils"; 7 | import Vuex, { Store } from "vuex"; 8 | import CommonComponents from "../../../components/common/index"; 9 | 10 | import EnterPasswordPage from "../../../components/Popup/EnterPasswordPage.vue"; 11 | import { loadI18nMessages } from "../../../store/i18n"; 12 | 13 | const should = chai.should(); 14 | chai.use(sinonChai); 15 | mocha.setup("bdd"); 16 | const localVue = createLocalVue(); 17 | 18 | describe("EnterPasswordPage", () => { 19 | before(async () => { 20 | localVue.prototype.i18n = await loadI18nMessages(); 21 | localVue.use(Vuex); 22 | for (const component of CommonComponents) { 23 | localVue.component(component.name, component.component); 24 | } 25 | }); 26 | 27 | let storeOpts = { 28 | modules: { 29 | accounts: { 30 | actions: { 31 | applyPassphrase: sinon.fake(), 32 | }, 33 | state: { 34 | wrongPassword: false, 35 | }, 36 | namespaced: true, 37 | }, 38 | }, 39 | }; 40 | let store: Store; 41 | 42 | beforeEach(() => { 43 | // TODO: find a nicer var 44 | storeOpts.modules.accounts.actions.applyPassphrase.resetHistory(); 45 | store = new Vuex.Store(storeOpts); 46 | }); 47 | 48 | it("should apply password when button is clicked", async () => { 49 | const wrapper = mount(EnterPasswordPage, { store, localVue }); 50 | 51 | const passwordInput = wrapper.find("input"); 52 | const passwordButton = wrapper.find("button"); 53 | 54 | passwordInput.setValue("somePassword"); 55 | await passwordButton.trigger("click"); 56 | storeOpts.modules.accounts.actions.applyPassphrase.should.have.been.calledWith( 57 | sinon.match.any, 58 | "somePassword" 59 | ); 60 | }); 61 | 62 | it("should apply password when enter is pressed", async () => { 63 | const wrapper = mount(EnterPasswordPage, { store, localVue }); 64 | 65 | const passwordInput = wrapper.find("input"); 66 | 67 | passwordInput.setValue("anotherPassword"); 68 | await passwordInput.trigger("keyup.enter"); 69 | storeOpts.modules.accounts.actions.applyPassphrase.should.have.been.calledWith( 70 | sinon.match.any, 71 | "anotherPassword" 72 | ); 73 | }); 74 | 75 | it("should autofocus password input", () => { 76 | const wrapper = mount(EnterPasswordPage, { 77 | store, 78 | localVue, 79 | attachToDocument: true, 80 | }); 81 | 82 | const passwordInput = wrapper.find("input"); 83 | 84 | passwordInput.element.should.eq(document.activeElement); 85 | }); 86 | 87 | it("should not show incorrect password message", () => { 88 | const wrapper = mount(EnterPasswordPage, { store, localVue }); 89 | 90 | const errorText = wrapper.find("label.warning"); 91 | 92 | errorText.isVisible().should.be.false; 93 | }); 94 | 95 | context("Incorrect password was entered", () => { 96 | before(() => { 97 | storeOpts.modules.accounts.state.wrongPassword = true; 98 | }); 99 | 100 | it("should show incorrect password message", () => { 101 | const wrapper = mount(EnterPasswordPage, { store, localVue }); 102 | 103 | const errorText = wrapper.find("label.warning"); 104 | 105 | errorText.isVisible().should.be.true; 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/test/components/Popup/MenuPage.test.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | import * as chai from "chai"; 3 | import { assert } from "chai"; 4 | import * as sinonChai from "sinon-chai"; 5 | import { createLocalVue, mount, Wrapper } from "@vue/test-utils"; 6 | import Vuex, { Store } from "vuex"; 7 | 8 | import { loadI18nMessages } from "../../../store/i18n"; 9 | import MenuPage from "../../../components/Popup/MenuPage.vue"; 10 | 11 | import { Style } from "../../../store/Style"; 12 | import { Accounts } from "../../../store/Accounts"; 13 | import { Backup } from "../../../store/Backup"; 14 | import { CurrentView } from "../../../store/CurrentView"; 15 | import { Menu } from "../../../store/Menu"; 16 | import { Notification } from "../../../store/Notification"; 17 | import { Qr } from "../../../store/Qr"; 18 | 19 | import chrome from "sinon-chrome"; 20 | 21 | chai.should(); 22 | chai.use(sinonChai); 23 | mocha.setup("bdd"); 24 | const localVue = createLocalVue(); 25 | 26 | describe("MenuPage", () => { 27 | before(async () => { 28 | localVue.prototype.i18n = await loadI18nMessages(); 29 | localVue.use(Vuex); 30 | }); 31 | 32 | let storeOpts = { 33 | menu: { 34 | state: { 35 | version: "1.2.3", 36 | }, 37 | namespaced: true, 38 | }, 39 | }; 40 | 41 | let store: Store<{}>; 42 | 43 | let wrapper: Wrapper; 44 | 45 | before(() => { 46 | // mock the chrome global object 47 | global.chrome.tabs.create = chrome.tabs.create; 48 | global.chrome.storage.managed.get = chrome.storage.managed.get; 49 | }); 50 | 51 | beforeEach(async () => { 52 | store = new Vuex.Store({ 53 | modules: storeOpts, 54 | }); 55 | wrapper = mount(MenuPage, { 56 | store, 57 | localVue, 58 | }); 59 | }); 60 | 61 | const clickMenuPageButtonByTitle = async ( 62 | wrapper: Wrapper, 63 | title: string 64 | ) => wrapper.find(`*[title='${title}']`).trigger("click"); 65 | 66 | describe("feedback button", () => { 67 | // mocks the user agent for testing purposes 68 | const mockUserAgent = (userAgent: string) => { 69 | Object.defineProperty(global, "navigator", { 70 | value: { 71 | userAgent, 72 | }, 73 | configurable: true, 74 | enumerable: true, 75 | writable: true, 76 | }); 77 | }; 78 | 79 | beforeEach(() => { 80 | wrapper = mount(MenuPage, { 81 | store, 82 | localVue, 83 | }); 84 | }); 85 | 86 | it("should open a new tab to the Chrome help page when the feedback button is clicked and the user agent is Chrome", async () => { 87 | mockUserAgent( 88 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)" 89 | ); 90 | await clickMenuPageButtonByTitle(wrapper, "Feedback"); 91 | assert.ok( 92 | chrome.tabs.create.withArgs({ url: "https://otp.ee/chromeissues" }) 93 | .calledOnce, 94 | "Tab create should be called with the Chrome URL" 95 | ); 96 | }); 97 | 98 | it("should open a new tab to the Edge help page when the feedback button is clicked and the user agent is Edge", async () => { 99 | mockUserAgent( 100 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43" 101 | ); 102 | await clickMenuPageButtonByTitle(wrapper, "Feedback"); 103 | assert.ok( 104 | chrome.tabs.create.withArgs({ url: "https://otp.ee/edgeissues" }) 105 | .calledOnce, 106 | "Tab create should be called with the Edge URL" 107 | ); 108 | }); 109 | 110 | it("should open a new tab to the Firefox help page when the feedback button is clicked and the user agent is Firefox", async () => { 111 | mockUserAgent( 112 | "Mozilla/5.0 (Windows NT x.y; rv:10.0) Gecko/20100101 Firefox/10.0" 113 | ); 114 | await clickMenuPageButtonByTitle(wrapper, "Feedback"); 115 | assert.ok( 116 | chrome.tabs.create.withArgs({ url: "https://otp.ee/firefoxissues" }) 117 | .calledOnce, 118 | "Tab create should be called with the Firefox URL" 119 | ); 120 | }); 121 | 122 | it("should open a new tab to the Chrome help page when the feedback button is clicked and the user agent is unknown", async () => { 123 | mockUserAgent("Unknown"); 124 | await clickMenuPageButtonByTitle(wrapper, "Feedback"); 125 | assert.ok( 126 | chrome.tabs.create.withArgs({ url: "https://otp.ee/chromeissues" }) 127 | .called, 128 | "Tab create should be called with the Chrome URL" 129 | ); 130 | }); 131 | 132 | describe("feedbackURL is set", () => { 133 | beforeEach(async () => { 134 | try { 135 | chrome.storage.managed.get.yieldsAsync({ 136 | feedbackURL: "https://authenticator.cc", 137 | }); 138 | 139 | store = new Vuex.Store({ 140 | modules: { 141 | backup: await new Backup().getModule(), 142 | currentView: new CurrentView().getModule(), 143 | notification: new Notification().getModule(), 144 | qr: new Qr().getModule(), 145 | style: new Style().getModule(), 146 | menu: await new Menu().getModule(), 147 | accounts: await new Accounts().getModule(), 148 | }, 149 | }); 150 | 151 | wrapper = mount(MenuPage, { 152 | store, 153 | localVue, 154 | }); 155 | } catch (e) { 156 | console.error(e); 157 | // Doesn't show up in mocha? 158 | throw e; 159 | } 160 | }); 161 | 162 | it("should open a new tab to the page specified in ManagedStorage", async () => { 163 | await clickMenuPageButtonByTitle(wrapper, "Feedback"); 164 | assert.ok( 165 | chrome.tabs.create.withArgs({ url: "https://authenticator.cc" }) 166 | .called, 167 | "Tab create should be called with the feedback URL" 168 | ); 169 | }); 170 | }); 171 | }); 172 | 173 | describe("extension version", () => { 174 | it("should be displayed", () => { 175 | assert.equal(wrapper.find("#version").text(), "Version 1.2.3"); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /src/test/gost.test.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | import * as chai from "chai"; 3 | import * as sinon from "sinon"; 4 | import * as sinonChai from "sinon-chai"; 5 | 6 | chai.use(sinonChai); 7 | mocha.setup("bdd"); 8 | 9 | import { 10 | AlgorithmIndentifier, 11 | GostDigest, 12 | gostEngine as GostEngine, 13 | } from "node-gost-crypto"; 14 | import { expect } from "chai"; 15 | import { KeyUtilities } from "../models/key-utilities"; 16 | import { OTPAlgorithm, OTPType, OTPUtil } from "../models/otp"; 17 | 18 | describe("Test GOST 2012", async () => { 19 | const secret: string = getRandomHEXString(32); 20 | const counter: number = calculateCounter(new Date()); 21 | await testAlgorithm(secret, counter, OTPAlgorithm.GOST3411_2012_256); 22 | await testAlgorithm(secret, counter, OTPAlgorithm.GOST3411_2012_512); 23 | }); 24 | 25 | function calculateCounter(date: Date) { 26 | const epoch: number = Math.round(date.getTime() / 1000.0); 27 | const period: number = 30; 28 | return Math.floor(epoch / period); 29 | } 30 | 31 | async function testAlgorithm( 32 | secret: string, 33 | counter: number, 34 | algorithm: OTPAlgorithm 35 | ) { 36 | const previousCounter = counter - 1; 37 | let alg: AlgorithmIndentifier; 38 | let cipher: GostDigest; 39 | alg = { 40 | mode: "HMAC", 41 | name: "GOST R 34.11", 42 | version: 2012, 43 | length: OTPUtil.getOTPAlgorithmSpec(algorithm).length, 44 | }; 45 | cipher = GostEngine.getGostDigest(alg); 46 | //current counter 47 | const signatureArray = new Uint8Array( 48 | cipher.sign( 49 | new Uint8Array(parseHexString(secret)), 50 | new Uint8Array(counterToArray(counter)) 51 | ) 52 | ); 53 | const signature = toHexString(signatureArray); 54 | const isSignatureOk = cipher.verify( 55 | new Uint8Array(parseHexString(secret)), 56 | signatureArray, 57 | new Uint8Array(counterToArray(counter)) 58 | ); 59 | const otp = getOtp(signature); 60 | //previous counter 61 | const prevSignatureArray = new Uint8Array( 62 | cipher.sign( 63 | new Uint8Array(parseHexString(secret)), 64 | new Uint8Array(counterToArray(previousCounter)) 65 | ) 66 | ); 67 | const prevSignature = toHexString(prevSignatureArray); 68 | const isPrevSignatureOk = cipher.verify( 69 | new Uint8Array(parseHexString(secret)), 70 | prevSignatureArray, 71 | new Uint8Array(counterToArray(previousCounter)) 72 | ); 73 | const previousOtp = getOtp(prevSignature); 74 | //check hash algorithm 75 | it( 76 | "(" + 77 | OTPAlgorithm[algorithm] + 78 | ") " + 79 | "hash from secret '" + 80 | secret + 81 | "' and counter '" + 82 | counter + 83 | "' = '" + 84 | signature + 85 | "', verifying", 86 | () => { 87 | expect(isSignatureOk).to.eq(true); 88 | } 89 | ); 90 | //check previous hash algorithm 91 | it( 92 | "(" + 93 | OTPAlgorithm[algorithm] + 94 | ") " + 95 | "hash from secret '" + 96 | secret + 97 | "' and counter '" + 98 | previousCounter + 99 | "' = '" + 100 | prevSignature + 101 | "', verifying", 102 | () => { 103 | expect(isPrevSignatureOk).to.eq(true); 104 | } 105 | ); 106 | //check otp is different from previous one 107 | it( 108 | "(" + 109 | OTPAlgorithm[algorithm] + 110 | ") " + 111 | "current otp = '" + 112 | otp + 113 | "', previous otp = '" + 114 | previousOtp + 115 | "', verifying otp codes are different", 116 | () => { 117 | expect(otp).to.not.eq(previousOtp); 118 | } 119 | ); 120 | //check otp generated is valid 121 | const _secret = "B1B0AE0E5ADFBF89A5F7DF440592A3AE"; //measuring 'secret' 122 | const _date = new Date("2021-01-01T00:00:00.000Z"); //measuring 'date' 123 | const _counter = calculateCounter(_date); 124 | const _otp = await KeyUtilities.generate( 125 | OTPType.hotp, 126 | _secret, 127 | _counter, 128 | 30, 129 | 6, 130 | algorithm 131 | ); 132 | let _validOtp = ""; 133 | if (algorithm === OTPAlgorithm.GOST3411_2012_256) { 134 | _validOtp = "982313"; //measuring 'otp' 135 | } 136 | if (algorithm === OTPAlgorithm.GOST3411_2012_512) { 137 | _validOtp = "733980"; //measuring 'otp' 138 | } 139 | it( 140 | "(" + 141 | OTPAlgorithm[algorithm] + 142 | ") " + 143 | "valid otp for secret '" + 144 | _secret + 145 | "' and date '" + 146 | _date + 147 | "' is '" + 148 | _validOtp + 149 | "', verifying", 150 | () => { 151 | expect(_otp).to.eq(_validOtp); 152 | } 153 | ); 154 | } 155 | 156 | function getOtp(signature: string) { 157 | const digits: number = 6; 158 | const offset = hex2dec(signature.substring(signature.length - 1)); 159 | let otp = 160 | (hex2dec(signature.substr(offset * 2, 8)) & hex2dec("7fffffff")) + ""; 161 | otp = otp.substr(otp.length - digits, digits); 162 | return otp; 163 | } 164 | 165 | function counterToArray(counter: number) { 166 | const data = []; 167 | let value = counter; 168 | for (let i = 8; i-- > 0; value >>>= 8) { 169 | data[i] = value & 0xff; 170 | } 171 | return data; 172 | } 173 | 174 | function getRandomHEXString(length: number) { 175 | const randomChars = "ABCDEF0123456789"; 176 | let result = ""; 177 | for (let i = 0; i < length; i++) { 178 | result += randomChars.charAt( 179 | Math.floor(Math.random() * randomChars.length) 180 | ); 181 | } 182 | return result; 183 | } 184 | 185 | function hex2dec(s: string) { 186 | return Number(`0x${s}`); 187 | } 188 | 189 | function parseHexString(str: string) { 190 | let result = []; 191 | while (str.length >= 8) { 192 | result.push(parseInt(str.substring(0, 8), 16)); 193 | str = str.substring(8, str.length); 194 | } 195 | return result; 196 | } 197 | 198 | function toHexString(byteArray: Uint8Array) { 199 | return Array.from(byteArray, function (byte) { 200 | return ("0" + (byte & 0xff).toString(16)).slice(-2); 201 | }).join(""); 202 | } 203 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export async function getSiteName() { 2 | const tab = await getCurrentTab(); 3 | const query = new URLSearchParams(document.location.search.substring(1)); 4 | 5 | let title: string | null; 6 | let url: string | null; 7 | const titleFromQuery = query.get("title"); 8 | const urlFromQuery = query.get("url"); 9 | 10 | if (titleFromQuery && urlFromQuery) { 11 | title = decodeURIComponent(titleFromQuery); 12 | url = decodeURIComponent(urlFromQuery); 13 | } else { 14 | if (!tab) { 15 | return [null, null]; 16 | } 17 | 18 | title = tab.title?.replace(/[^a-z0-9]/gi, "").toLowerCase() ?? null; 19 | url = tab.url ?? null; 20 | } 21 | 22 | if (!url) { 23 | return [title, null]; 24 | } 25 | 26 | const urlParser = new URL(url); 27 | const hostname = urlParser.hostname; // it's always lower case 28 | 29 | // try to parse name from hostname 30 | // i.e. hostname is www.example.com 31 | // name should be example 32 | let nameFromDomain = ""; 33 | 34 | // ip address 35 | if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { 36 | nameFromDomain = hostname; 37 | } 38 | 39 | // local network 40 | if (hostname.indexOf(".") === -1) { 41 | nameFromDomain = hostname; 42 | } 43 | 44 | const hostLevelUnits = hostname.split("."); 45 | 46 | if (hostLevelUnits.length === 2) { 47 | nameFromDomain = hostLevelUnits[0]; 48 | } 49 | 50 | // www.example.com 51 | // example.com.cn 52 | if (hostLevelUnits.length > 2) { 53 | // example.com.cn 54 | if ( 55 | ["com", "net", "org", "edu", "gov", "co"].indexOf( 56 | hostLevelUnits[hostLevelUnits.length - 2] 57 | ) !== -1 58 | ) { 59 | nameFromDomain = hostLevelUnits[hostLevelUnits.length - 3]; 60 | } else { 61 | // www.example.com 62 | nameFromDomain = hostLevelUnits[hostLevelUnits.length - 2]; 63 | } 64 | } 65 | 66 | nameFromDomain = nameFromDomain.replace(/-/g, "").toLowerCase(); 67 | 68 | return [title, nameFromDomain, hostname]; 69 | } 70 | 71 | export function getMatchedEntries( 72 | siteName: Array, 73 | entries: OTPEntryInterface[] 74 | ) { 75 | if (siteName.length < 2) { 76 | return false; 77 | } 78 | 79 | const matched = []; 80 | 81 | for (const entry of entries) { 82 | if (isMatchedEntry(siteName, entry)) { 83 | matched.push(entry); 84 | } 85 | } 86 | 87 | return matched; 88 | } 89 | 90 | export function getMatchedEntriesHash( 91 | siteName: Array, 92 | entries: OTPEntryInterface[] 93 | ) { 94 | const matchedEnteries = getMatchedEntries(siteName, entries); 95 | if (matchedEnteries) { 96 | return matchedEnteries.map((entry) => entry.hash); 97 | } 98 | 99 | return false; 100 | } 101 | 102 | function isMatchedEntry( 103 | siteName: Array, 104 | entry: OTPEntryInterface 105 | ) { 106 | if (!entry.issuer) { 107 | return false; 108 | } 109 | 110 | const issuerHostMatches = entry.issuer.split("::"); 111 | const issuer = issuerHostMatches[0].replace(/[^0-9a-z]/gi, "").toLowerCase(); 112 | 113 | if (!issuer) { 114 | return false; 115 | } 116 | 117 | const siteTitle = siteName[0] || ""; 118 | const siteNameFromHost = siteName[1] || ""; 119 | const siteHost = siteName[2] || ""; 120 | 121 | if (issuerHostMatches.length > 1) { 122 | if (siteHost && siteHost.indexOf(issuerHostMatches[1]) !== -1) { 123 | return true; 124 | } 125 | } 126 | // site title should be more detailed 127 | // so we use siteTitle.indexOf(issuer) 128 | if (siteTitle && siteTitle.indexOf(issuer) !== -1) { 129 | return true; 130 | } 131 | 132 | if (siteNameFromHost && issuer.indexOf(siteNameFromHost) !== -1) { 133 | return true; 134 | } 135 | 136 | return false; 137 | } 138 | 139 | export async function getCurrentTab() { 140 | const currentWindow = await chrome.windows.getCurrent(); 141 | const queryOptions = { active: true, windowId: currentWindow.id }; 142 | // `tab` will either be a `tabs.Tab` instance or `undefined`. 143 | const [tab] = await chrome.tabs.query(queryOptions); 144 | return tab; 145 | } 146 | 147 | interface TabWithIdAndURL extends chrome.tabs.Tab { 148 | id: number; 149 | url: string; 150 | } 151 | 152 | export function okToInjectContentScript( 153 | tab: chrome.tabs.Tab 154 | ): tab is TabWithIdAndURL { 155 | return ( 156 | tab.id !== undefined && 157 | tab.url !== undefined && 158 | (tab.url.startsWith("https://") || 159 | tab.url.startsWith("http://") || 160 | tab.url.startsWith("file://")) 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /svg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | arrow-left 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/bars.svg: -------------------------------------------------------------------------------- 1 | 2 | Bars 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/check.svg: -------------------------------------------------------------------------------- 1 | 2 | Check 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/clipboard-check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svg/code.svg: -------------------------------------------------------------------------------- 1 | 2 | Code 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/cog.svg: -------------------------------------------------------------------------------- 1 | 2 | cog 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/comments.svg: -------------------------------------------------------------------------------- 1 | 2 | comments 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/database.svg: -------------------------------------------------------------------------------- 1 | 2 | Database 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/exchange.svg: -------------------------------------------------------------------------------- 1 | 2 | Alternate Exchange 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | Globe with Americas shown 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/info.svg: -------------------------------------------------------------------------------- 1 | 2 | Info 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/key-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svg/lightbulb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svg/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | lock 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/minus-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | Minus Circle 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | Alternate Pencil 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | thumbtack 3 | 4 | -------------------------------------------------------------------------------- /svg/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | plus 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/qrcode.svg: -------------------------------------------------------------------------------- 1 | 2 | qrcode 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | Alternate Redo 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/scan.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /svg/sync.svg: -------------------------------------------------------------------------------- 1 | 2 | Alternate Sync 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/wrench.svg: -------------------------------------------------------------------------------- 1 | 2 | Wrench 3 | 4 | 5 | -------------------------------------------------------------------------------- /svg/x-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | Times Circle 3 | 4 | 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2020.Promise", "es6", "dom"], 4 | "target": "es6", 5 | "strict": true, 6 | "module": "es2015", 7 | "rootDir": "src", 8 | "moduleResolution": "node", 9 | "outDir": "build", 10 | "allowUnreachableCode": false, 11 | "allowUnusedLabels": false, 12 | "declaration": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmitOnError": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "pretty": true, 18 | "sourceMap": true, 19 | "allowSyntheticDefaultImports": true, 20 | }, 21 | "include": [ 22 | "src/*.ts", 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ], 28 | "vueCompilerOptions": { 29 | "target": 2 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /view/argon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /view/import.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /view/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /view/permissions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /view/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /view/qrdebug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | QR Debugging 5 | 6 | 7 | 8 |

QR Scan Debugging Page

9 |
10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /view/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { VueLoaderPlugin } = require("vue-loader"); 3 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 4 | 5 | module.exports = { 6 | mode: "development", 7 | devtool: "source-map", 8 | entry: { 9 | argon: "./src/argon.ts", 10 | background: "./src/background.ts", 11 | content: "./src/content.ts", 12 | popup: "./src/popup.ts", 13 | import: "./src/import.ts", 14 | options: "./src/options.ts", 15 | qrdebug: "./src/qrdebug.ts", 16 | permissions: "./src/permissions.ts", 17 | }, 18 | module: { 19 | noParse: /\.wasm$/, 20 | rules: [ 21 | { 22 | // argon2-browser overrides 23 | test: /\.wasm$/, 24 | loader: "base64-loader", 25 | type: "javascript/auto" 26 | }, 27 | { 28 | test: /\.tsx?$/, 29 | loader: "ts-loader", 30 | options: { 31 | appendTsSuffixTo: [/\.vue$/], 32 | transpileOnly: true 33 | }, 34 | exclude: /node_modules/ 35 | }, 36 | { 37 | test: /\.vue$/, 38 | loader: "vue-loader" 39 | }, 40 | { 41 | test: /\.svg$/, 42 | loader: 'vue-svg-loader' 43 | }, 44 | { 45 | test: /\.(png|jpe?g|gif)$/, 46 | use: [ 47 | { 48 | loader: 'url-loader', 49 | options: {}, 50 | } 51 | ] 52 | } 53 | ], 54 | }, 55 | plugins: [ 56 | new VueLoaderPlugin(), 57 | new ForkTsCheckerWebpackPlugin({ 58 | typescript: { 59 | extensions: { 60 | vue: true 61 | } 62 | } 63 | }) 64 | ], 65 | resolve: { 66 | extensions: [ 67 | ".mjs", 68 | ".js", 69 | ".jsx", 70 | ".vue", 71 | ".json", 72 | ".wasm", 73 | ".ts", 74 | ".tsx" 75 | ], 76 | modules: ["node_modules"], 77 | fallback: { 78 | // Stop argon2-browser from trying to bring in node modules 79 | fs: false, 80 | path: false 81 | } 82 | }, 83 | output: { 84 | path: path.resolve(__dirname, "dist"), 85 | publicPath: "/dist/" 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { merge } = require('webpack-merge'); 3 | const common = require('./webpack.config.js'); 4 | const { ProvidePlugin } = require("webpack"); 5 | 6 | module.exports = merge(common, { 7 | entry: { 8 | test: "./src/test.ts", 9 | }, 10 | // Polyfills for mocha and sinon 11 | resolve: { 12 | fallback: { 13 | "util": require.resolve("util/"), 14 | "buffer": require.resolve('buffer/'), 15 | "stream": require.resolve("stream-browserify") 16 | } 17 | }, 18 | plugins: [ 19 | new ProvidePlugin({ 20 | process: 'process/browser.js', 21 | }), 22 | new ProvidePlugin({ 23 | Buffer: ['buffer', 'Buffer'], 24 | }), 25 | ], 26 | module: { 27 | // to suppress mocha warnings 28 | exprContextCritical: false, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.config.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | }); 7 | -------------------------------------------------------------------------------- /webpack.watch.js: -------------------------------------------------------------------------------- 1 | // TODO: this is broken because webpack-extension-reloader does not support webpack 5. 2 | 3 | const path = require('path'); 4 | const merge = require('webpack-merge'); 5 | const dev = require('./webpack.dev.js'); 6 | const ExtensionReloader = require('webpack-extension-reloader'); 7 | const {exec} = require('child_process'); 8 | 9 | // after compiling, the tests will automatically be run each time a file change occurs 10 | const runTestsAfterBuild = () => { 11 | return { 12 | apply: (compiler) => { 13 | compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => { 14 | // leave as node otherwise browser does not launch 15 | exec('node scripts/test-runner.js', (err, stdout, stderr) => { 16 | if (stdout) process.stdout.write(stdout); 17 | if (stderr) process.stderr.write(stderr); 18 | }); 19 | }); 20 | } 21 | } 22 | }; 23 | 24 | module.exports = merge(dev, { 25 | mode: 'development', 26 | plugins: [ 27 | new ExtensionReloader(), 28 | runTestsAfterBuild(), 29 | ], 30 | watch: true, 31 | watchOptions: { 32 | ignored: /node_modules/ 33 | }, 34 | output: { 35 | path: path.resolve(__dirname, 'test/chrome/dist'), 36 | publicPath: '/test/chrome/dist/' 37 | } 38 | }); 39 | --------------------------------------------------------------------------------