Repository: buttercup/buttercup-browser-extension
Branch: master
Commit: 4a5e2c359fb4
Files: 165
Total size: 334.7 KB
Directory structure:
gitextract_eo89ckcu/
├── .editorconfig
├── .github/
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .prettierrc
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── LICENSE
├── PRIVACY_POLICY.md
├── README.md
├── package.json
├── resources/
│ ├── full.pug
│ ├── manifest.v2.json
│ ├── manifest.v3.json
│ └── popup.pug
├── scripts/
│ └── version.js
├── source/
│ ├── background/
│ │ ├── index.ts
│ │ ├── library/
│ │ │ └── domain.ts
│ │ ├── services/
│ │ │ ├── autoLogin.ts
│ │ │ ├── config.ts
│ │ │ ├── crypto.ts
│ │ │ ├── cryptoKeys.ts
│ │ │ ├── desktop/
│ │ │ │ ├── actions.ts
│ │ │ │ ├── header.ts
│ │ │ │ └── request.ts
│ │ │ ├── disabledDomains.ts
│ │ │ ├── entry.ts
│ │ │ ├── init.ts
│ │ │ ├── log.ts
│ │ │ ├── loginMemory.ts
│ │ │ ├── messaging.ts
│ │ │ ├── notifications.ts
│ │ │ ├── recents.ts
│ │ │ ├── storage/
│ │ │ │ └── BrowserStorageInterface.ts
│ │ │ ├── storage.ts
│ │ │ └── tabs.ts
│ │ └── types.ts
│ ├── full/
│ │ ├── applications/
│ │ │ └── full.tsx
│ │ ├── components/
│ │ │ ├── App.tsx
│ │ │ ├── Layout.tsx
│ │ │ └── pages/
│ │ │ ├── AttributionsPage.tsx
│ │ │ ├── DisabledDomainsPage.tsx
│ │ │ ├── NotificationsPage.tsx
│ │ │ ├── connect/
│ │ │ │ ├── CodeInput.tsx
│ │ │ │ ├── ConnectPage.tsx
│ │ │ │ └── index.tsx
│ │ │ └── saveCredentials/
│ │ │ ├── CredentialsSaver.tsx
│ │ │ ├── CredentialsSelector.tsx
│ │ │ ├── NewEntrySavePrompt.tsx
│ │ │ └── index.tsx
│ │ ├── hooks/
│ │ │ ├── credentials.ts
│ │ │ ├── disabledDomains.ts
│ │ │ ├── document.ts
│ │ │ └── vaultContents.ts
│ │ ├── index.pug
│ │ ├── services/
│ │ │ ├── credentials.ts
│ │ │ ├── disabledDomains.ts
│ │ │ ├── init.ts
│ │ │ ├── log.ts
│ │ │ ├── notifications.ts
│ │ │ └── vaults.ts
│ │ ├── styles/
│ │ │ └── full.sass
│ │ └── types.ts
│ ├── popup/
│ │ ├── applications/
│ │ │ └── popup.tsx
│ │ ├── components/
│ │ │ ├── App.tsx
│ │ │ ├── contexts/
│ │ │ │ └── LaunchContext.tsx
│ │ │ ├── entries/
│ │ │ │ ├── EntryInfoDialog.tsx
│ │ │ │ ├── EntryItem.tsx
│ │ │ │ └── EntryItemList.tsx
│ │ │ ├── navigation/
│ │ │ │ └── Navigator.tsx
│ │ │ ├── otps/
│ │ │ │ ├── OTPItem.tsx
│ │ │ │ └── OTPItemList.tsx
│ │ │ ├── pages/
│ │ │ │ ├── AboutPage.tsx
│ │ │ │ ├── EntriesPage.tsx
│ │ │ │ ├── OTPsPage.tsx
│ │ │ │ ├── SaveDialogPage.tsx
│ │ │ │ ├── SettingsPage.tsx
│ │ │ │ └── VaultsPage.tsx
│ │ │ └── vaults/
│ │ │ ├── VaultItem.tsx
│ │ │ ├── VaultItemList.tsx
│ │ │ └── VaultStateIndicator.tsx
│ │ ├── hooks/
│ │ │ ├── credentials.ts
│ │ │ ├── desktop.ts
│ │ │ ├── document.ts
│ │ │ ├── otp.ts
│ │ │ └── tab.ts
│ │ ├── index.pug
│ │ ├── queries/
│ │ │ ├── desktop.ts
│ │ │ ├── disabledDomains.ts
│ │ │ └── loginMemory.ts
│ │ ├── services/
│ │ │ ├── clipboard.ts
│ │ │ ├── entry.ts
│ │ │ ├── init.ts
│ │ │ ├── log.ts
│ │ │ ├── recents.ts
│ │ │ ├── reset.ts
│ │ │ └── tab.ts
│ │ ├── state/
│ │ │ └── app.ts
│ │ ├── styles/
│ │ │ └── popup.sass
│ │ └── types.ts
│ ├── shared/
│ │ ├── components/
│ │ │ ├── ConfirmDialog.tsx
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── ErrorMessage.tsx
│ │ │ ├── RouteError.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ └── loading/
│ │ │ └── BusyLoader.tsx
│ │ ├── extension.ts
│ │ ├── hooks/
│ │ │ ├── async.ts
│ │ │ ├── config.ts
│ │ │ ├── global.ts
│ │ │ ├── theme.ts
│ │ │ └── timer.ts
│ │ ├── i18n/
│ │ │ ├── trans.ts
│ │ │ └── translations/
│ │ │ ├── en.json
│ │ │ └── nl.json
│ │ ├── library/
│ │ │ ├── buffer.ts
│ │ │ ├── clone.ts
│ │ │ ├── domain.ts
│ │ │ ├── error.ts
│ │ │ ├── extension.ts
│ │ │ ├── i18n.ts
│ │ │ ├── log.ts
│ │ │ ├── otp.ts
│ │ │ ├── url.ts
│ │ │ ├── vaultTypes.ts
│ │ │ └── version.ts
│ │ ├── notifications/
│ │ │ ├── index.ts
│ │ │ └── pages/
│ │ │ └── WelcomeV3.tsx
│ │ ├── queries/
│ │ │ └── config.ts
│ │ ├── services/
│ │ │ ├── messaging.ts
│ │ │ └── notifications.ts
│ │ ├── styles/
│ │ │ ├── base.sass
│ │ │ └── fonts.sass
│ │ ├── symbols.ts
│ │ ├── themes.ts
│ │ └── types.ts
│ ├── tab/
│ │ ├── index.ts
│ │ ├── library/
│ │ │ ├── disable.ts
│ │ │ ├── dismount.ts
│ │ │ ├── frames.ts
│ │ │ ├── page.ts
│ │ │ ├── position.ts
│ │ │ ├── resize.ts
│ │ │ ├── styles.ts
│ │ │ └── zIndex.ts
│ │ ├── services/
│ │ │ ├── LoginTracker.ts
│ │ │ ├── autoLogin.ts
│ │ │ ├── config.ts
│ │ │ ├── form.ts
│ │ │ ├── formDetection.ts
│ │ │ ├── init.ts
│ │ │ ├── log.ts
│ │ │ ├── logins/
│ │ │ │ ├── disabled.ts
│ │ │ │ ├── saving.ts
│ │ │ │ └── watcher.ts
│ │ │ └── messaging.ts
│ │ ├── state/
│ │ │ ├── form.ts
│ │ │ └── frame.ts
│ │ ├── types.ts
│ │ └── ui/
│ │ ├── launch.ts
│ │ ├── popup.ts
│ │ └── saveDialog.ts
│ └── typings/
│ ├── assets.d.ts
│ └── globals.d.ts
├── tsconfig.json
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
[{.prettierrc,package.json,package-lock.json,.babelrc}]
indent_size = 2
================================================
FILE: .github/workflows/test.yml
================================================
name: Tests
on: push
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Node.js specs ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run test:format
release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- name: Node.js specs ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run release
================================================
FILE: .gitignore
================================================
*.log
.DS_Store
.history
node_modules
/dist
/release
Archive.zip
dist.zip
/secrets.json
================================================
FILE: .prettierrc
================================================
{
"printWidth": 120,
"tabWidth": 4,
"trailingComma": "none"
}
================================================
FILE: .vscode/settings.json
================================================
{
"vsicons.presets.angular": false,
"eslint.enable": false,
"files.associations": {
"**/*.js": "javascriptreact"
},
"typescript.preferences.importModuleSpecifierEnding": "js"
}
================================================
FILE: CHANGELOG.md
================================================
# Buttercup browser extension changelog
## v3.2.0
_2024-04-09_
* Input button customisation (global)
* Dutch language (not yet selectable)
* **Bugfix**:
* Some fields not filled correctly (partial fix)
* Login save prompt shown for existing credentials
## v3.1.0
_2024-03-27_
* ([#467](https://github.com/buttercup/buttercup-browser-extension/issues/467)) Entry info popup
* **Bugfix**:
* ([#469](https://github.com/buttercup/buttercup-browser-extension/issues/469)) Forms recognised as login forms when they should not be
* ([#466](https://github.com/buttercup/buttercup-browser-extension/issues/466)) Bad OTP URIs crash application
* Entry page search does nothing when first opening the extension
## v3.0.0
_2024-03-26_
* **Major rewrite**
* Requires desktop application for vault access
* Nested iframe traversal
* OTP support
## v2.26.0
_2023-11-08_
* **Important version 3 update notice**
* Updates for core libraries and datasources
## v2.25.3
_2023-01-31_
* **Bugfix**:
* Google Drive would fail when tokens expire (new response format)
## v2.25.2
_2022-09-03_
* **Bugfix**:
* Fixed Dropbox connectivity issues
* Fixed Google Drive re-authentication loop, short auth time
## v2.25.1
_2022-08-16_
* **Bugfix**:
* Format B saving new properties would fail
* Format B conversion would omit history
## v2.25.0
_2022-06-02_
* Buttercup upgrade: v6
* Improved Dropbox/Google Drive integrations
* Improved vault stability and performance
* Improved support for Vault Format B
* Removed My Buttercup integration
* Vault editor page redesign
* Updated vault UI
## v2.24.3
_2021-05-24_
* **Bugfix**:
* Vault pages not taking up 100% of the height of the window
## v2.24.2
_2021-05-22_
* **Bugfix**:
* ([#381](https://github.com/buttercup/buttercup-browser-extension/issues/381)) Search results not showing most recent result (URL detection)
* WebDAV connection crashes tab during failed connection attempt (when adding a vault)
* **Critical auto-update issue**: Core crash when receiving updated vaults in the background
## v2.24.1
_2021-01-06_
* Remove `activeTab` permission requirement
* **Bugfix**:
* ([#393](https://github.com/buttercup/buttercup-browser-extension/issues/393)) Copy to clipboard not working for in-page dialog
## v2.24.0
_2020-12-09_
* Site icons for results within in-page search dialog
* Performance improvements regarding search
* Vault unlock/save performance improvements
## v2.23.1
_2020-11-27_
* **Bugfix**:
* ([#368](https://github.com/buttercup/buttercup-browser-extension/issues/368)) Popup / Dialog menus very slow to open (performance bugfix for search)
## v2.23.0
_2020-09-05_
* Dynamic icons defaults to **enabled**
* Removed dynamic icons setting popup page
* **Bugfix**:
* ([#366](https://github.com/buttercup/buttercup-browser-extension/issues/366)) Google Drive bad refresh-token method call
## v2.22.0
_2020-09-04_
* Core group/entry lookup performance upgrades
* **Bugfix**:
* ([#370](https://github.com/buttercup/buttercup-browser-extension/issues/370)) Critical CPU/memory use after some time
* Entries not able to be moved from group to group
## v2.21.0
_2020-08-30_
* **Buttercup Core v5**
* Improved performance
* Improved stability
* Future support for **Vault Format B**
* Dynamic icons for entries (optional)
* Reduced extension size (< 50% of the size of 2.20.2)
* Removed `Buffer` dependencies
* **Bugfix**:
* Search wouldn't work (no results)
## v2.20.2
_2020-08-19_
* **Bugfix**:
* ([buttercup-core#287](https://github.com/buttercup/buttercup-core/issues/287)) Vaults grow to enormous size
## v2.20.1
_2020-08-03_
* **Attachments** (My Buttercup)
* Add, remove, preview and download attachments when using My Buttercup vaults
* Core memory/stability improvements when merging vault changes from remote sources
## v2.19.0
_2020-07-25_
* New Buttercup form button behaviour (to improve login form stability)
* Clipboard-writing permission for certain browsers
* Improved auto-update stability
## v2.18.0
_2020-07-07_
* Search results won't show items in trash
* **Bugfix**:
* ([#337](https://github.com/buttercup/buttercup-browser-extension/pull/337)) No login-save-prompt when entry selected for login form (includes auto login)
## v2.17.0
_2020-07-05_
* New search functionality
* Result scoring per domain
* Vault type icons on unlock-all-vaults page
## v2.16.2
_2020-07-02_
* **Bugfix**:
* Unable to enter form details when selecting entry result in dialog
## v2.16.1
_2020-07-01_
* **Bugfix**:
* Unable to select vault in save credentials form
* No search results in popup dialog
* Broken vault lock state in menu
## v2.16.0
_2020-06-30_
* Core version 4
* My Buttercup datasource support
* ([#340](https://github.com/buttercup/buttercup-browser-extension/pull/340)) Allow localhost in disabled domains
## v2.15.1
_2020-04-02_
* **Bugfix**:
* WebDAV would fail to connect on some services, such as ownCloud
## v2.15.0
_2020-03-18_
* Disable save prompt for domains
* Memory for all login form inputs
* **Bugfix**:
* Buttons would disappear from some forms (Dropbox)
## v2.14.0
_2020-02-03_
* Upgrade webdav for reduced application size
* **Bugfix**:
* ([#325](https://github.com/buttercup/buttercup-browser-extension/issues/325)) New vaults fail to create
* ([#286](https://github.com/buttercup/buttercup-browser-extension/issues/286)) Unlock-vaults page not opening in FF after clicking button in on-page dialog
## v2.13.1
_2020-01-24_
* **Bugfix**:
* ([#324](https://github.com/buttercup/buttercup-browser-extension/issues/324)) Very slow vault contents navigation
* ([#323](https://github.com/buttercup/buttercup-browser-extension/issues/323)) OTP (HOTP) failures crashing entire vault management UI
* ([#322](https://github.com/buttercup/buttercup-browser-extension/issues/322)) Auto-update of search results by URL not working
## v2.13.0
_2020-01-22_
* Group context menu: creation, renaming, moving and deletion
* New group in root button
* Entry field history (basic)
* **Bugfix**:
* Credit card entry type would crash application
* State sync for vault management interface inconsistent
## v2.12.0
_2020-01-18_
* ([#320](https://github.com/buttercup/buttercup-browser-extension/issues/320)) Open permissions option when adding Google Drive vaults
* **Bugfix**:
* ([#270](https://github.com/buttercup/buttercup-browser-extension/issues/270)) _(2nd attempt)_: Support multiple Google accounts
* ([#319](https://github.com/buttercup/buttercup-browser-extension/issues/319)) Google Drive authentication not working in Microsoft Edge (unofficial patch - pre-release)
## v2.11.1
_2020-01-08_
* **Bugfix**:
* ([#316](https://github.com/buttercup/buttercup-browser-extension/issues/316)) `createSession` is not defined (local file host vaults)
## v2.11.0
_2020-01-05_
* Core integration with App-Env for web-based crypto improvement
* ([#270](https://github.com/buttercup/buttercup-browser-extension/issues/270)) Prompt for account-selection on Google authentication (Google Drive) to support multiple accounts
* ([#245](https://github.com/buttercup/buttercup-browser-extension/issues/245)) Google Drive permissions reduced - Only files touched by Buttercup are accessible to the application
* **Bugfix**:
* ([#314](https://github.com/buttercup/buttercup-browser-extension/issues/314)) Unable to open Google Drive vault
## v2.10.1
_2019-12-26_
* **Bugfix**:
* ([#312](https://github.com/buttercup/buttercup-browser-extension/issues/312)) No login prompt visible on Firefox
## v2.10.0
_2019-12-24_
* Ability to change vault password
* **Bugfix**:
* ([#307](https://github.com/buttercup/buttercup-browser-extension/issues/307)) Cannot save new note-type entry (or any other custom types)
## v2.9.0
_2019-11-12_
* My Buttercup preparation
* ownCloud/Nextcloud removed in favour of WebDAV (existing connections should still function)
* ([#95](https://github.com/buttercup/buttercup-browser-extension/issues/95)) Context menus to choose credentials for form-filling and login
## v2.8.2
_2019-09-01_
* **Bugfix**:
* ([#269](https://github.com/buttercup/buttercup-browser-extension/issues/269)) Password field in popup menu not copy-able and always visible
## v2.8.1
_2019-09-01_
* **Bugfix**:
* ([#253](https://github.com/buttercup/buttercup-browser-extension/issues/253)) Vault saving via editing UI (second attempt)
## v2.8.0
_2019-07-23_
* **Bugfix**:
* ~~([#253](https://github.com/buttercup/buttercup-browser-extension/issues/253)) Vault saving via editing UI~~
* ([#259](https://github.com/buttercup/buttercup-browser-extension/pull/259)) General improvements to the add-vault page
* Unlock button on in-page dialog when vaults are locked
* Unlock button for single-vault now navigates to edit page
* TOTP / HOTP support via vault UI (display only, no form-fill)
* Entry value type support via vault UI
* Updated Dropbox/Google Drive clients for compatibiltiy
## v2.7.0
_2019-04-28_
* Vault editing interface
## v2.6.0
_2019-04-14_
* ([#246](https://github.com/buttercup/buttercup-browser-extension/issues/246)) Google Drive refresh token support
## v2.5.1
_2019-03-13_
* **Bugfix**:
* ([#244](https://github.com/buttercup/buttercup-browser-extension/issues/244)) Google Drive fetching fails on large directories
## v2.5.0
_2019-03-09_
* **Google Drive** support
* Unlock-vaults button in popup
## v2.4.1
_2019-02-05_
* **Bugfix**:
* Regression in auto-unlock functionality
## v2.4.0
_2019-02-04_
* ([#235](https://github.com/buttercup/buttercup-browser-extension/issues/235)) Use local (static) icons and don't request them from remote sources
* ([#171](https://github.com/buttercup/buttercup-browser-extension/issues/171)) Auto-lock vaults after a configurable time
* Auto-login button in popup menu
## v2.3.1
_2019-01-19_
* **Bugfixes**:
* ([#214](https://github.com/buttercup/buttercup-browser-extension/issues/214)) Popup menu layout broken for long items
* ([#216](https://github.com/buttercup/buttercup-browser-extension/issues/216)) Autofocus extension popover search input
## v2.3.0
_2018-01-12_
* **Bugfixes**:
* ([#217](https://github.com/buttercup/buttercup-browser-extension/issues/217)) Popup menu layout broken
* ([#218](https://github.com/buttercup/buttercup-browser-extension/issues/218)) Some website forms not recognised
* Improved entry details UI in popup menus
* Improved entry results in popup menus
* What's New section on auto-unlock page
* Improved login form detection
## v2.2.0
_2019-01-05_
* ([#212](https://github.com/buttercup/buttercup-browser-extension/issues/212)) Poor search results performance
* ([#202](https://github.com/buttercup/buttercup-browser-extension/issues/202)) Auto-unlock setting not working
## v2.1.3
_2018-12-16_
* ([#190](https://github.com/buttercup/buttercup-browser-extension/issues/190)) Dropbox connection never completes loading procedure (UI spinner)
## v2.1.2
_2018-12-08_
* ([#203](https://github.com/buttercup/buttercup-browser-extension/issues/203)) Failure saving Dropbox changes
## v2.1.1
_2018-11-27_
* **Bugfix**: ownCloud / Nextcloud / WebDAV vaults could not be added (Chrome)
## v2.1.0
_2018-11-24_
* ([#91](https://github.com/buttercup/buttercup-browser-extension/issues/91)) Connect through desktop application (local filesystem access)
* New WebDAV client
* New Dropbox client
## v2.0.0
_2018-10-29_
* **Major UI overhaul**
* ([#180](https://github.com/buttercup/buttercup-browser-extension/issues/180)) Option to disable "save-new" dialog
* ([#160](https://github.com/buttercup/buttercup-browser-extension/issues/160)) Settings page
* ([#173](https://github.com/buttercup/buttercup-browser-extension/issues/173)) Source type is empty - display glitches (final cleanup)
* Dark/Light mode themes
* Setting for showing the auto-unlock page (default on)
* Vault/Account sync via Chrome/Firefox's account logins (sync storage)
* Show/Copy entry properties in dialog/popup menus
## v1.12.1
_2018-10-18_
* ([#173](https://github.com/buttercup/buttercup-browser-extension/issues/173)) Source type is empty - display glitches
* ([#182](https://github.com/buttercup/buttercup-browser-extension/issues/182)) Add bundling process for Chrome/Firefox
## v1.12.0
_2018-10-07_
* ([#110](https://github.com/buttercup/buttercup-browser-extension/issues/110)) Reload archives after some time
* ([#174](https://github.com/buttercup/buttercup-browser-extension/issues/174)) Unable to access via WebDAV (Seafile)
## v1.11.1
_2018-08-27_
* ([#164](https://github.com/buttercup/buttercup-browser-extension/issues/164)) `t is null` error when unlocking archives
## v1.11.0
_2018-08-24_
* ([#136](https://github.com/buttercup/buttercup-browser-extension/issues/136)) Use last generated password form context menu
* ([#153](https://github.com/buttercup/buttercup-browser-extension/issues/153)) **Bugfix**: Button layout issues
* Upgraded login form targetting
## v1.10.0
_2018-07-11_
* New popup menu design
* Search and open items from the popup
## v1.9.1
_2018-07-08_
* ([#152](https://github.com/buttercup/buttercup-browser-extension/issues/152)) **Bugfix**: Failure while adding WebDAV archives
## v1.9.0
_2018-06-29_
* ([#131](https://github.com/buttercup/buttercup-browser-extension/issues/131)) Upgrade core to v2
* New archive format (supporting future encryption standards)
* ([#130](https://github.com/buttercup/buttercup-browser-extension/issues/130)) Auto unlock prompt shown when browser is opened
* ([#147](https://github.com/buttercup/buttercup-browser-extension/issues/147)) Remove settings page link
## v1.8.0
_2018-06-22_
* Upgrade core to 1.7.1
* Future proofing for archive format
## v1.7.0
_2018-04-28_
* ([#124](https://github.com/buttercup/buttercup-browser-extension/issues/124)) Simplify save-new login screen by removing password confirmation
* ([#141](https://github.com/buttercup/buttercup-browser-extension/issues/141)) Buttercup launch button layout issues
* Dependency updates
## v1.6.2
_2018-04-24_
* ([#139](https://github.com/buttercup/buttercup-browser-extension/issues/139)) No "Generate password" option shown when right-clicking inputs (Firefox/Chrome)
## v1.6.1
_2018-04-01_
* ([#137](https://github.com/buttercup/buttercup-browser-extension/issues/137)) Unable to close save-new dialog
## v1.6.0
_2018-04-01_
* "password" type input support for Firefox and the password generator
* ([#134](https://github.com/buttercup/buttercup-browser-extension/issues/134)) Password generator "Use this" button fails on Firefox
* ([#133](https://github.com/buttercup/buttercup-browser-extension/issues/133)) Password generator bad padding issue
## v1.5.0
_2018-03-31_
* Password generator
* Right-click context menu
## v1.4.0
_2018-03-23_
* ([#126](https://github.com/buttercup/buttercup-browser-extension/issues/126)) Add an attribute to allow inputs and forms to be ignored by Buttercup
* Support new login forms
* Update login form detection priorities
## v1.3.1
_2018-02-20_
* ([#121](https://github.com/buttercup/buttercup-browser-extension/issues/121)) Unable to click "Save" for new logins
## v1.3.0
_2018-02-05_
* Use `chrome.storage` for better data persistence
## v1.2.1
_2018-02-04_
* Update code splitting configuration (Firefox submission fixes)
## v1.1.1
_2018-02-04_
* Improve URL filtering for new credentials saving
## v1.1.0
_2018-02-04_
* ([#112](https://github.com/buttercup/buttercup-browser-extension/issues/106)) Save newly-entered credentials
## v1.0.7
_2018-01-22_
* Add data collection event listeners for form attachments
## v1.0.6
_2018-01-21_
* Fix component communication in Firefox
* Add storage permission
## v1.0.5
_2018-01-21_
* ([#106](https://github.com/buttercup/buttercup-browser-extension/issues/106)) Fix Nextcloud archive searching
## v1.0.4
_2018-01-20_
* First 1.* release for Firefox
* Fix character encoding
* Fix button icon not showing on some sites
* Fix scrollbars in popup
## v1.0.3
_2018-01-19_
* Fix GitHub login
* Fix logins that have multiple detected inputs
## v1.0.2
_2018-01-18_
* ([#97](https://github.com/buttercup/buttercup-browser-extension/issues/97)) Fixed Twitter login bug
## **v1.0.1**
_2018-01-18_
* Full re-work of the entire extension
* **Nextcloud** official support
* ([#92](https://github.com/buttercup/buttercup-browser-extension/issues/92)) Fixed security vulnerability
## v0.14.2
_2017-07-15_
* Bugfix: [ownCloud subfolder installations: Subfolder ignored](https://github.com/buttercup/buttercup-browser-extension/issues/80)
## v0.14.1
_2017-06-24_
* Bugfix: [Unable to connect to certain WebDAV services](https://github.com/buttercup/buttercup-desktop/issues/303)
## v0.14.0
_2017-06-07_
* Bugfix: [Malformed URL error](https://github.com/buttercup/buttercup-browser-extension/issues/71) - old WebDAV client caused issues with special characters in passwords
* Add back old Buttercup-core-web classes for compatibility
## v0.13.1
_2017-05-27_
* Bugfix: Archive creation in root level
## v0.13.0
_2017-04-23_
* Added right-click context menu drilldown for choosing which entry to fill form with
* Updated hotkey for auto-login (Command+Shift+L for Mac, Ctrl+Shift+L for Windows/Linux)
* Fixed deprecation warnings during build and updated some packages
## v0.12.1
_2017-04-19_
* Spring clean:
* Reduced permissions requirements in manifest
* Improved last submitted form security
## v0.12.0
_2017-04-15_
* Bugfix: Popup formatting issues
* Added hotkey for auto-login (Command+B (Mac) / Ctrl+B (Windows/Linux))
## v0.11.0
_2017-04-14_
* Bugfix: Clicking "No" on save prompt would not cancel further popups
* Bugfix: Form submission error when selecting credentials
* Prefill title when saving new credentials
## v0.10.0
_2017-03-31_
* Added right-click context menu on form inputs
* Fixed overflow on remote filesystem explorer when adding archives
* Finalised styling on unlock-archive form
## v0.9.0
_2017-03-29_
* Improved UI for popup password list (archive + groups pathing)
* Bugfix: Fixed wrong-password during unlock sequence breaking state
## v0.8.0
_2017-03-22_
* Fuzzy searching for entries on login-forms
* Login-form popup available on password fields
## v0.7.0
_2017-03-15_
* **Support for Firefox**
* Auto-submit when selecting credentials on a form
* Styles normalisation
## v0.6.0
_2017-03-14_
* Upgrade core
* Use serialisation for archive properties
## v0.5.0
_2017-03-09_
* Upgrade core
* Drastically increase PBKDF2 rounds
* Improve URI matching
## v0.4.0
_2017-02-11_
* First alpha release
## v0.3.0
* Pre-release development build
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 Perry Mitchell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: PRIVACY_POLICY.md
================================================
# Privacy Policy
This privacy policy concerns the Browser Extension for Buttercup, its use and the data it makes use of.
## About Buttercup
Buttercup is a software suite designed to provide a secure application with which to store highly-sensitive information (such as account credentials) in encrypted vault files. Great care is taken to ensure that all secure data remains protected, with as little information as possible being handled by the application and the Buttercup platform therein.
### Terms
The following terms will appear throughout this document and their meaning is important to grasp for a proper understanding of this policy.
| Term | Description |
|-------------------|-------------------------------------------------------|
| Archive | See "vault". |
| Call-to-action | A button or link that indicates to the user that it performs some kind of action. A button with "Login" on it is a good example. |
| Encryption | The process of transforming vault contents into a secure, un-readable format for storage that can only be _read_ by providing a master password. |
| Master password | The highly secure secret password used to lock and unlock vaults, known only to the user. |
| Vault | An encrypted password vault, stored as a file either locally or remotely (on some kind of service). |
### Types of data
Vaults, in their locked state, contain **encrypted data** which represent secret information that Buttercup uses to function (passwords etc.). When unlocked, the data is **unencrypted** and resides in memory on the user's device.
Buttercup applications may store **unencrypted configuration information** on the device in a standard directory or location. Configuration data does not include any sensitive information. Configuration refers to settings that allow the application to function in a desired manner for the user.
Buttercup for Browsers does not collect any **analytics data**, but the hosting platforms (eg. Mozilla/Google) may collect anonymous analytics with regards to the extension itself (installations vs uninstallations, etc.).
Due to the fact that Buttercup interacts with webpages, the **Document Object Model (DOM)** may be modified by its use.
## Data storage and transfer with regards to 3rd Parties
### Remote vault storage
Buttercup vault files, especially with regards to this browser extension, are stored remotely on hosting services. These services (eg. Dropbox, Nextcloud etc.) utilise their own privacy policies and standards, and are responsible for the files stored within their platform. Vaults transferred from the extension to these services are encrypted **before** they leave the application. No unencrypted data is stored on these remote services, besides the filename itself.
It should be noted that connection logs may be kept on various services. It is the responsibility of the user to take care as to what services they choose to use and connect to, if any.
### DOM (Document Object Model)
The browser extention modifies the DOM of open webpages so that it can track several different items while the page is live:
* Detected login forms
* Detected inputs that can be used for login (username/email, password etc.)
* Submit buttons
* Potential submit buttons (tracking text that resembles login call-to-actions)
The extension may add attributes to existing elements, as well as adding entirely new elements (eg. buttons to open the Buttercup login menu). These modifications may indicate to the site, as well as to scripts running on the site, the fact that the current user has the Buttercup browser extension installed.
### Analytics
Buttercup does not track any analytics directly. Platforms that Buttercup uses, such as Mozilla (for Firefox addons) or Google (for Chrome extensions), may track analytics that relate to the use of the extension in an anonymous manner. Buttercup makes use of this data for the purpose of analysing product health and potential market expansions.
## Internal data storage and use
### In-memory vaults
When the vaults are decrypted using the user's master password, the contents of the vault are loaded into memory within the browser. The contents of the vaults are stored within the extension and are not accessible outside of the extension's context. Raw credentials may be transferred to webpages not owned by Buttercup at the request of the user when trying to log in to a website.
## Your responsibility as a user
By using Buttercup's applications or services, you acknowledge that Buttercup stores highly-sensitive information for you using a **master password**. This password must not be shared with anyone, and should sufficiently strong to ensure that vault integrity is not broken if a bad actor gains access to it. Both the password and the encrypted vault file are the responsibility of the user, and both should be treated as highly sensitive.
## Our responsibility to you, the user
We provide Buttercup as free-to-use software - we take every reasonable precaution to ensure that the vaults are encrypted in a manner that is secure by industry standards. We must ensure that only encrypted data leaves the user's device when storing vaults. We must ensure that only the bare minimum information is shared with 3rd party services to allow Buttercup to integrate with them.
================================================
FILE: README.md
================================================
# Buttercup Browser Extension
Buttercup credentials manager extension for the browser.
[](https://buttercup.pw)  [](https://chrome.google.com/webstore/detail/buttercup/heflipieckodmcppbnembejjmabajjjj?hl=en-GB) [](https://chrome.google.com/webstore/detail/buttercup/heflipieckodmcppbnembejjmabajjjj?hl=en-GB) [](https://addons.mozilla.org/en-US/firefox/addon/buttercup-pw/) [](https://addons.mozilla.org/en-US/firefox/addon/buttercup-pw/) [](https://keybase.io/team/bcup)
---
⚠️ **Project Closure** ⚠️
The Buttercup project has come to an end, and these repositories are in transition to becoming public archives. No public-facing resources will be removed, wherever possible. Please do not create issues or PRs - they will unfortunately be ignored. Discussion can be found [here](https://github.com/buttercup/buttercup-desktop/discussions/1395), and explanation [here](https://gist.github.com/perry-mitchell/43ebfcec4d874b77a704be1d4f2262e6).
---
## About
This browser extension allows users to interface with password archives authored by the [Buttercup password manager](https://github.com/buttercup-pw/buttercup) (it _requires_ v2.26 and later of the desktop application to be installed to function).
The extension makes secured requests to the desktop application for information within its unlocked vaults, and makes those credentials available within the browser. It is also able to save new logins from the browser extension as they're recognised. Besides a username and password, the extension can also enter OTP codes when required.
### Forms & Logins
Buttercup for Browsers auto-detects some login forms and login inputs, allowing the user to auto-fill them at their discretion. This extension uses [Locust](https://github.com/buttercup/locust) under the hood to **detect forms and inputs** (any issues with detecting forms and inputs should be opened there).
### Supported browsers
[Chrome](https://chrome.google.com/webstore/detail/buttercup/heflipieckodmcppbnembejjmabajjjj?hl=en-GB), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/buttercup-pw/), [Edge](https://www.microsoft.com/en-us/edge) (version 79+) and [Brave](https://chrome.google.com/webstore/detail/buttercup/heflipieckodmcppbnembejjmabajjjj) are supported.
_Some browsers, such as **Brave** for example, will be able to install Buttercup via the Google Chrome web store._
Other browsers will be supported in order of request/popularity. Issues created for unsupported browsers, or for browsers not on the roadmap, may be closed without warning.
**Opera** is not supported due to their incredibly slow and unreliable release process. We will not be adding support for Opera.
### Integrated platforms
The extension allows for connections to several services, such as Dropbox and Google Drive. The extension supports whatever platforms the desktop application does, including local vaults.
#### Supported platforms
The browsers listed above, running on Windows, Mac or Linux on a desktop platform. This extension is not supported on any mobile or tablet devices.
### Usage
The browser extension can be controlled from the **popup menu**, which is launched by pressing the Buttercup button in the browser menu. This menu displays a list of archives as well as settings and other items.
When viewing pages that contain login forms, Buttercup can assist logging in when you interact with the login buttons (displayed beside detected login inputs).
Buttercup can also remember new logins, which are detected as they occur.
You can **block** Buttercup from detecting forms and inputs by applying the attribute `data-bcupignore=true`:
```html
```
### Development
Development of features and bugfixes is supported in the following environment:
* NodeJS version 20 (latest minor version)
* Linux / Mac
* Tested in at least Chrome / Firefox
To set up your development environment:
* Clone this repo
* Ensure API keys are available (Google Drive)
* Execute `npm install` inside the project directory
* Run `npm run dev:chrome` or similar
* Load the unpacked `dist` folder in your browser addons
#### Chrome
Run the following to develop the extension:
1. Execute `npm run dev:chrome` to build and watch the project (to build production code, execute `npm run build`)
2. Go to [chrome://extensions](chrome://extensions) and enable _"Developer mode"_
3. Select the new button _"Load unpacked"_, then select the `./dist` directory built on step 1
#### Firefox
Run the following to develop the extension:
* Execute `npm run dev:firefox` to build and watch the project (to build production code, execute `npm run release`)
#### Releasing
To build release-ready zip archives, run the command `npm run release` after having set up the development environment. The archives will be written to `release/(browser)` where `(browser)` is the browser type. Archives named `extension.zip` contain the built extension sourcecode and `source.zip` contains the raw source.
### Adding to Chrome
You can load an **unpacked extension** in Chrome by navigating to [chrome://extensions/](chrome://extensions/). Simply locate the project's directory and use **dist/** as the extension directory.
### Adding to Firefox
You can load an **unpacked extension** in Firefox by navigating to [about:debugging](about:debugging). Click "Load Temporary Add-on" and locate the project's directory, using **dist/** as the extension directory.
================================================
FILE: package.json
================================================
{
"name": "buttercup-browser-extension",
"version": "3.2.0",
"description": "Buttercup browser extension",
"exports": "./dist/background/index.js",
"type": "module",
"types": "./dist/background/index.d.ts",
"scripts": {
"build": "run-s set-version build:production",
"build:chrome": "BROWSER=chrome npm run build",
"build:edge": "BROWSER=edge npm run build",
"build:firefox": "BROWSER=firefox npm run build",
"build:production": "webpack --mode production --progress --config webpack.config.js",
"clean": "rimraf dist/* release/*",
"dev": "npm run clean && npm run set-version && webpack --mode development -w --progress --config webpack.config.js",
"dev:chrome": "BROWSER=chrome npm run dev",
"dev:edge": "BROWSER=edge npm run dev",
"dev:firefox": "concurrently -k \"BROWSER=firefox npm run dev\" \"cd dist && web-ext run\" --restart-tries 20 --restart-after 5000 --devtools --keep-profile-changes",
"format": "prettier --write '{{source,test}/**/*.{ts,js},webpack.config.js}'",
"release": "run-s clean release:chrome release:firefox release:edge",
"release:chrome": "npm run build:chrome && mkdirp release/chrome && zip -r release/chrome/extension.zip ./dist",
"release:edge": "npm run build:edge && mkdirp release/edge && zip -r release/edge/extension.zip ./dist",
"release:firefox": "npm run build:firefox && mkdirp release/firefox && run-s release:firefox:extension release:firefox:source",
"release:firefox:extension": "cd dist && web-ext build --overwrite-dest && cd .. && mv ./dist/web-ext-artifacts/*.zip ./release/firefox/",
"release:firefox:source": "zip -r release/firefox/source.zip . --exclude=/.git* --exclude=/node_modules* --exclude=/.history* --exclude=/dist* --exclude=/release* --exclude=*.DS_Store*",
"set-version": "node scripts/version.js",
"test": "run-s test:format",
"test:format": "prettier --check '{{source,test}/**/*.{ts,js},webpack.config.js}'"
},
"lint-staged": {
"{{source,test}/**/*.{ts,js},webpack.config.js}": [
"prettier --write"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/buttercup/buttercup-browser-extension.git"
},
"keywords": [
"buttercup",
"password",
"vault",
"login",
"secure"
],
"author": "Perry Mitchell ",
"license": "MIT",
"bugs": {
"url": "https://github.com/buttercup/buttercup-browser-extension/issues"
},
"homepage": "https://github.com/buttercup/buttercup-browser-extension#readme",
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/preset-env": "^7.23.3",
"@babel/preset-react": "^7.23.3",
"@blueprintjs/core": "^4.20.2",
"@blueprintjs/icons": "^4.16.0",
"@blueprintjs/popover2": "^1.14.11",
"@blueprintjs/select": "^4.9.24",
"@buttercup/channel-queue": "^1.4.0",
"@buttercup/locust": "^2.3.1",
"@buttercup/ui": "^6.2.2",
"@types/chrome": "^0.0.251",
"@types/ms": "^0.7.34",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"babel-loader": "^9.1.3",
"buttercup": "^7.6.0",
"classnames": "^2.2.6",
"concurrently": "^8.2.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"eventemitter3": "^5.0.1",
"expiry-map": "^2.0.0",
"file-loader": "^6.0.0",
"gle": "^1.0.3",
"husky": "^4.2.5",
"i18next": "^23.7.6",
"iocane": "^5.1.1",
"layerr": "^2.0.0",
"lint-staged": "^15.1.0",
"mkdirp": "^3.0.1",
"ms": "^2.1.3",
"mucus": "^1.0.0",
"npm-run-all": "^4.1.5",
"obstate": "^0.1.4",
"on-navigate": "^0.1.1",
"otpauth": "^9.1.5",
"parse-domain": "^8.0.2",
"prettier": "^3.1.0",
"pug-plugin": "^5.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-obstate": "^0.1.3",
"react-router-dom": "^6.21.3",
"redom": "^3.29.1",
"resolve-typescript-plugin": "^2.0.1",
"rimraf": "^5.0.5",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"styled-components": "^5.3.11",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"ulidx": "^2.2.1",
"url-join": "^5.0.0",
"url-loader": "^4.1.1",
"web-ext": "^7.8.0",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-merge": "^5.10.0"
}
}
================================================
FILE: resources/full.pug
================================================
doctype html
html
head
title Buttercup
meta(charset="utf-8")
link(rel="icon", type="image/png", href=require("./buttercup-256.png").default)
link(rel="stylesheet" href="full.css")
link(rel="stylesheet" href="vendors.css")
script(defer, src="full.js")
script(defer, src="vendors.js")
body
div#root
================================================
FILE: resources/manifest.v2.json
================================================
{
"manifest_version": 2,
"name": "Buttercup",
"description": "Browser extension for Buttercup, the secure and easy-to-use password manager.",
"version": "0.0.0",
"icons": {
"256": "manifest-res/buttercup-256.png",
"128": "manifest-res/buttercup-128.png",
"48": "manifest-res/buttercup-48.png",
"16": "manifest-res/buttercup-16.png"
},
"background": {
"scripts": [
"background.js"
]
},
"browser_action": {
"default_icon": "manifest-res/buttercup-256.png",
"default_popup": "popup.html#/"
},
"content_scripts" : [
{
"matches": ["http://*/*", "https://*/*"],
"run_at": "document_end",
"all_frames": true,
"js": ["tab.js"]
}
],
"permissions": [
"clipboardWrite",
"http://*/*",
"https://*/*",
"storage",
"tabs",
"unlimitedStorage"
],
"web_accessible_resources": [
"*.png",
"*.jpg"
],
"applications": {
"gecko": {
"id": "{10e7d273-2e63-47c9-82af-76c45dc1b624}"
}
}
}
================================================
FILE: resources/manifest.v3.json
================================================
{
"manifest_version": 3,
"name": "Buttercup",
"description": "Browser extension for Buttercup, the secure and easy-to-use password manager.",
"version": "0.0.0",
"icons": {
"256": "manifest-res/buttercup-256.png",
"128": "manifest-res/buttercup-128.png",
"48": "manifest-res/buttercup-48.png",
"16": "manifest-res/buttercup-16.png"
},
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "Buttercup",
"default_icon": "manifest-res/buttercup-256.png",
"default_popup": "/popup.html#/"
},
"content_scripts" : [
{
"matches": ["http://*/*", "https://*/*"],
"run_at": "document_end",
"all_frames": true,
"js": ["tab.js"]
}
],
"permissions": [
"clipboardWrite",
"storage",
"tabs",
"unlimitedStorage"
],
"web_accessible_resources": [
{
"resources": ["*.png", "*.jpg"],
"matches": ["http://*/*", "https://*/*"]
},
{
"resources": ["popup.html"],
"matches": ["http://*/*", "https://*/*"]
}
]
}
================================================
FILE: resources/popup.pug
================================================
doctype html
html
head
title Menu ⋅ Buttercup
meta(charset="utf-8")
link(rel="icon", type="image/png", href=require("./buttercup-256.png").default)
link(rel="stylesheet" href="popup.css")
link(rel="stylesheet" href="vendors.css")
script(defer, src="popup.js")
script(defer, src="vendors.js")
body
div#root
================================================
FILE: scripts/version.js
================================================
import { readFileSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fileURLToPath } from "url";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const packageInfo = JSON.parse(readFileSync(
resolve(__dirname, "../package.json"),
"utf8"
));
const buildDate = new Date();
const built = `${buildDate.getUTCFullYear()}-${String(buildDate.getUTCMonth() + 1).padStart(2, "0")}-${String(buildDate.getUTCDate()).padStart(2, "0")}`;
const output = `// Do not edit this file - it is generated automatically at build time
export const BUILD_DATE = "${built}";
export const VERSION = "${packageInfo.version}";
`;
writeFileSync(
resolve(__dirname, "../source/shared/library/version.ts"),
output
);
================================================
FILE: source/background/index.ts
================================================
import { initialise } from "./services/init.js";
import { log } from "./services/log.js";
initialise().catch((err) => {
console.error(err);
log("initialisation failed");
});
================================================
FILE: source/background/library/domain.ts
================================================
import { UsedCredentials } from "../types.js";
export function extractDomainFromCredentials(credentials: UsedCredentials): string | null {
const match = /^https?:\/\/([^\/]+)/.exec(credentials.url);
return match ? match[1] : null;
}
================================================
FILE: source/background/services/autoLogin.ts
================================================
import { SearchResult } from "buttercup";
import ExpiryMap from "expiry-map";
interface RegisteredItem {
entry: SearchResult;
tabID: number;
}
const REGISTER_MAX_AGE = 30 * 1000; // 30 seconds
let __register: ExpiryMap | null = null;
export function getAutoLoginForTab(tabID: number): SearchResult | null {
const register = getRegister();
const key = `tab-${tabID}`;
if (register.has(key)) {
const item = (register.get(key) as RegisteredItem).entry;
register.delete(key);
return item;
}
return null;
}
function getRegister(): ExpiryMap {
if (!__register) {
__register = new ExpiryMap(REGISTER_MAX_AGE);
}
return __register;
}
export function registerAutoLogin(entry: SearchResult, tabID: number): void {
const register = getRegister();
register.set(`tab-${tabID}`, { entry, tabID });
}
================================================
FILE: source/background/services/config.ts
================================================
import { getSyncValue, setSyncValue } from "./storage.js";
import { Configuration, InputButtonType, SyncStorageItem } from "../types.js";
import { naiveClone } from "../../shared/library/clone.js";
const DEFAULTS: Configuration = {
entryIcons: true,
inputButtonDefault: InputButtonType.LargeButton,
saveNewLogins: true,
theme: "light",
useSystemTheme: true
};
let __lastConfig: Configuration | null = null;
export function getConfig(): Configuration {
if (!__lastConfig) {
throw new Error("No configuration available");
}
return __lastConfig;
}
export async function initialise() {
__lastConfig = await updateConfigWithDefaults();
}
export async function updateConfigValue(key: T, value: Configuration[T]): Promise {
const configRaw = await getSyncValue(SyncStorageItem.Configuration);
const config = configRaw ? JSON.parse(configRaw) : naiveClone(DEFAULTS);
config[key] = value;
__lastConfig = config;
await setSyncValue(SyncStorageItem.Configuration, JSON.stringify(config));
}
async function updateConfigWithDefaults(): Promise {
let configRaw = await getSyncValue(SyncStorageItem.Configuration);
const config = configRaw ? JSON.parse(configRaw) : { ...DEFAULTS };
for (const key in DEFAULTS) {
if (typeof config[key] === "undefined") {
config[key] = DEFAULTS[key];
}
}
await setSyncValue(SyncStorageItem.Configuration, JSON.stringify(config));
return config;
}
================================================
FILE: source/background/services/crypto.ts
================================================
import { EncryptionAlgorithm, createAdapter } from "iocane";
import { deriveSecretKey, importECDHKey } from "./cryptoKeys.js";
export async function decryptPayload(
payload: string,
sourcePublicKey: string,
targetPrivateKey: string
): Promise {
const privateKey = await importECDHKey(targetPrivateKey);
const publicKey = await importECDHKey(sourcePublicKey);
const secret = await deriveSecretKey(privateKey, publicKey);
return createAdapter().decrypt(payload, secret) as Promise;
}
export async function encryptPayload(
payload: string,
sourcePrivateKey: string,
targetPublicKey: string
): Promise {
const privateKey = await importECDHKey(sourcePrivateKey);
const publicKey = await importECDHKey(targetPublicKey);
const secret = await deriveSecretKey(privateKey, publicKey);
return createAdapter()
.setAlgorithm(EncryptionAlgorithm.GCM)
.setDerivationRounds(100000)
.encrypt(payload, secret) as Promise;
}
================================================
FILE: source/background/services/cryptoKeys.ts
================================================
import { ulid } from "ulidx";
import { log } from "./log.js";
import { getLocalValue, setLocalValue } from "./storage.js";
import { arrayBufferToHex } from "../../shared/library/buffer.js";
import { API_KEY_ALGO, API_KEY_CURVE } from "../../shared/symbols.js";
import { LocalStorageItem } from "../types.js";
import { Layerr } from "layerr";
async function createKeys(): Promise<{
privateKey: string;
publicKey: string;
}> {
const { privateKey, publicKey } = await window.crypto.subtle.generateKey(
{
name: API_KEY_ALGO,
namedCurve: API_KEY_CURVE
},
true,
["deriveKey"]
);
log("generating public and private key pair for browser auth");
const privateKeyStr = await exportECDHKey(privateKey);
const publicKeyStr = await exportECDHKey(publicKey);
log("generated new browser auth keys");
return {
privateKey: privateKeyStr,
publicKey: publicKeyStr
};
}
export async function deriveSecretKey(privateKey: CryptoKey, publicKey: CryptoKey): Promise {
let cryptoKey: CryptoKey;
try {
cryptoKey = await window.crypto.subtle.deriveKey(
{
name: API_KEY_ALGO,
public: publicKey
},
privateKey,
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
);
} catch (err) {
throw new Layerr(err, "Failed deriving secret key");
}
const exported = await window.crypto.subtle.exportKey("raw", cryptoKey);
return arrayBufferToHex(exported);
}
async function exportECDHKey(key: CryptoKey): Promise {
try {
const exported = await window.crypto.subtle.exportKey("jwk", key);
return JSON.stringify(exported);
} catch (err) {
throw new Layerr(err, "Failed exporting ECDH key");
}
}
export async function generateKeys(): Promise {
let [apiPrivate, apiPublic, clientID] = await Promise.all([
getLocalValue(LocalStorageItem.APIPrivateKey),
getLocalValue(LocalStorageItem.APIPublicKey),
getLocalValue(LocalStorageItem.APIClientID)
]);
if (apiPrivate && apiPublic && clientID) return;
// Regenerate
log("api keys missing: will generate");
const { privateKey, publicKey } = await createKeys();
clientID = ulid();
await setLocalValue(LocalStorageItem.APIPrivateKey, privateKey);
await setLocalValue(LocalStorageItem.APIPublicKey, publicKey);
await setLocalValue(LocalStorageItem.APIClientID, clientID);
}
export async function importECDHKey(key: string): Promise {
let jwk: JsonWebKey;
try {
jwk = JSON.parse(key) as JsonWebKey;
} catch (err) {
throw new Layerr(err, "Failed importing ECDH key");
}
const usages: Array = jwk.key_ops && jwk.key_ops.includes("deriveKey") ? ["deriveKey"] : [];
return window.crypto.subtle.importKey(
"jwk",
jwk,
{
name: API_KEY_ALGO,
namedCurve: API_KEY_CURVE
},
true,
usages
);
}
================================================
FILE: source/background/services/desktop/actions.ts
================================================
import { Layerr } from "layerr";
import { EntryID, EntryType, GroupID, SearchResult, VaultFacade, VaultSourceID } from "buttercup";
import { getLocalValue } from "../storage.js";
import { sendDesktopRequest } from "./request.js";
import { generateAuthHeader } from "./header.js";
import { LocalStorageItem, OTP, VaultSourceDescription, VaultsTree } from "../../types.js";
export async function authenticateBrowserAccess(code: string): Promise {
const localPublicKey = await getLocalValue(LocalStorageItem.APIPublicKey);
const clientID = await getLocalValue(LocalStorageItem.APIClientID);
if (!localPublicKey) {
throw new Error("No local public key available");
}
if (!clientID) {
throw new Error("No API client ID set");
}
const { publicKey } = (await sendDesktopRequest({
method: "POST",
route: "/v1/auth/response",
payload: {
code,
id: clientID,
publicKey: localPublicKey
}
})) as { publicKey: string };
if (!publicKey) {
throw new Layerr("No server public key received from browser authentication");
}
return publicKey;
}
export async function getEntrySearchResults(
entries: Array<{ entryID: EntryID; sourceID: VaultSourceID }>
): Promise> {
const authHeader = await generateAuthHeader();
const { results } = (await sendDesktopRequest({
method: "POST",
route: "/v1/entries/specific",
auth: authHeader,
payload: {
entries
}
})) as {
results: Array;
};
return results;
}
export async function getOTPs(): Promise> {
const authHeader = await generateAuthHeader();
const { otps } = (await sendDesktopRequest({
method: "GET",
route: "/v1/otps",
auth: authHeader
})) as {
otps: Array;
};
return otps;
}
export async function getVaultSources(): Promise> {
const authHeader = await generateAuthHeader();
const { sources } = (await sendDesktopRequest({
method: "GET",
route: "/v1/vaults",
auth: authHeader
})) as {
sources: Array;
};
return sources;
}
export async function getVaultsTree(): Promise {
const authHeader = await generateAuthHeader();
const { names, tree } = (await sendDesktopRequest({
method: "GET",
route: "/v1/vaults-tree",
auth: authHeader
})) as {
names?: Record;
tree: Record;
};
return Object.keys(tree).reduce((output, sourceID) => {
return {
...output,
[sourceID]: {
...tree[sourceID],
name: names?.[sourceID] ?? "Untitled vault"
}
};
}, {});
}
export async function hasConnection(): Promise {
const serverPublicKey = await getLocalValue(LocalStorageItem.APIServerPublicKey);
return !!serverPublicKey;
}
export async function initiateConnection(): Promise {
await sendDesktopRequest({
method: "POST",
route: "/v1/auth/request",
payload: {
client: "browser",
purpose: "vaults-access",
rev: 1
}
});
}
export async function promptSourceLock(sourceID: VaultSourceID): Promise {
const authHeader = await generateAuthHeader();
const status = await sendDesktopRequest({
method: "POST",
route: `/v1/vaults/${sourceID}/lock`,
auth: authHeader,
output: "status"
});
return status === 200;
}
export async function promptSourceUnlock(sourceID: VaultSourceID): Promise {
const authHeader = await generateAuthHeader();
await sendDesktopRequest({
method: "POST",
route: `/v1/vaults/${sourceID}/unlock`,
auth: authHeader
});
}
export async function saveExistingEntry(
sourceID: VaultSourceID,
groupID: GroupID,
entryID: EntryID,
properties: Record
): Promise {
const authHeader = await generateAuthHeader();
await sendDesktopRequest({
method: "PATCH",
route: `/v1/vaults/${sourceID}/group/${groupID}/entry/${entryID}`,
auth: authHeader,
payload: {
properties
}
});
}
export async function saveNewEntry(
sourceID: VaultSourceID,
groupID: GroupID,
entryType: EntryType,
properties: Record
): Promise {
const authHeader = await generateAuthHeader();
const { entryID } = (await sendDesktopRequest({
method: "POST",
route: `/v1/vaults/${sourceID}/group/${groupID}/entry`,
auth: authHeader,
payload: {
properties,
type: entryType
}
})) as {
entryID: EntryID;
};
return entryID;
}
export async function searchEntriesByURL(url: string): Promise> {
const authHeader = await generateAuthHeader();
const { results } = (await sendDesktopRequest({
method: "GET",
route: "/v1/entries",
payload: {
type: "url",
url
},
auth: authHeader
})) as {
results: Array;
};
return results;
}
export async function searchEntriesByTerm(term: string): Promise> {
const authHeader = await generateAuthHeader();
const { results } = (await sendDesktopRequest({
method: "GET",
route: "/v1/entries",
payload: {
type: "term",
term
},
auth: authHeader
})) as {
results: Array;
};
return results;
}
export async function testAuth(): Promise {
const authHeader = await generateAuthHeader();
try {
await sendDesktopRequest({
method: "POST",
route: "/v1/auth/test",
payload: {
client: "browser",
purpose: "vaults-access",
rev: 1
},
auth: authHeader
});
} catch (err) {
console.error(err);
throw new Layerr(err, "Desktop connection failed");
}
}
================================================
FILE: source/background/services/desktop/header.ts
================================================
import { Layerr } from "layerr";
import { LocalStorageItem } from "../../types.js";
import { getLocalValue } from "../storage.js";
export async function generateAuthHeader(): Promise {
const clientID = await getLocalValue(LocalStorageItem.APIClientID);
if (!clientID) {
throw new Layerr(
{
info: {
i18n: "error.code.desktop-connection-not-authorised"
}
},
"No API client ID set"
);
}
return `Client ${clientID}`;
}
================================================
FILE: source/background/services/desktop/request.ts
================================================
import { Layerr } from "layerr";
import joinURL from "url-join";
import { DESKTOP_API_PORT } from "../../../shared/symbols.js";
import { decryptPayload, encryptPayload } from "../crypto.js";
import { getLocalValue } from "../storage.js";
import { LocalStorageItem } from "../../types.js";
type OutputType = "body" | "status" | undefined;
interface DesktopRequestConfig {
auth?: string | null;
method: string;
output?: O;
payload?: Record | null;
route: string;
}
const DESKTOP_URL_BASE = `http://localhost:${DESKTOP_API_PORT}`;
export async function sendDesktopRequest(
config: DesktopRequestConfig
): Promise>;
export async function sendDesktopRequest(config: DesktopRequestConfig): Promise;
export async function sendDesktopRequest(
config: DesktopRequestConfig
): Promise>;
export async function sendDesktopRequest(
config: DesktopRequestConfig
): Promise | number> {
const { auth = null, method, output = "body", payload = null, route } = config;
// Prepare un-encrypted configuration first
let url = joinURL(DESKTOP_URL_BASE, route);
const requestConfig: RequestInit = {
method,
headers: {}
};
if (payload !== null) {
if (/^get$/i.test(method)) {
const newURL = new URL(url);
for (const prop in payload) {
if (payload.hasOwnProperty(prop)) {
newURL.searchParams.set(prop, payload[prop]);
}
}
url = newURL.toString();
} else {
requestConfig.body = JSON.stringify(payload);
Object.assign(requestConfig.headers as HeadersInit, {
"Content-Type": "application/json"
});
}
}
if (auth !== null) {
requestConfig.headers = requestConfig.headers || {};
// Request requires encryption, perform setup now
requestConfig.headers["Authorization"] = auth;
if (typeof requestConfig.body === "string") {
requestConfig.headers["X-Content-Type"] = requestConfig.headers["Content-Type"];
requestConfig.headers["Content-Type"] = "text/plain";
// Encrypt
const privateKey = await getLocalValue(LocalStorageItem.APIPrivateKey);
const publicKey = await getLocalValue(LocalStorageItem.APIServerPublicKey);
if (!privateKey) {
throw new Error("Authenticated request failed: No private key available");
}
if (!publicKey) {
throw new Error("Authenticated request failed: No public key available");
}
requestConfig.body = await encryptPayload(requestConfig.body, privateKey, publicKey);
}
}
// Make request
const resp = await fetch(url, requestConfig);
if (!resp.ok) {
throw new Layerr(
{
info: {
code: "desktop-request-failed",
status: resp.status,
statusText: resp.statusText
}
},
`Desktop request failed: ${resp.status} ${resp.statusText}`
);
}
if (output === "status") {
return resp.status;
}
// Handle encrypted response
if (resp.headers.get("X-Bcup-API")) {
const components = resp.headers.get("X-Bcup-API")?.split(",") ?? [];
if (components.includes("enc")) {
const content = await resp.text();
const contentType = resp.headers.get("X-Content-Type") || resp.headers.get("Content-Type") || "text/plain";
// Decrypt
const privateKey = await getLocalValue(LocalStorageItem.APIPrivateKey);
const publicKey = await getLocalValue(LocalStorageItem.APIServerPublicKey);
if (!privateKey) {
throw new Error("Decrypting response failed: No private key available");
}
if (!publicKey) {
throw new Error("Decrypting response failed: No public key available");
}
const rawDecrypted = await decryptPayload(content, publicKey, privateKey);
return /application\/json/.test(contentType) ? JSON.parse(rawDecrypted) : rawDecrypted;
}
}
// Standard, unencrypted response
if (/application\/json/.test(resp.headers.get("Content-Type") ?? "")) {
return resp.json();
}
return resp.text();
}
================================================
FILE: source/background/services/disabledDomains.ts
================================================
import { getSyncValue, setSyncValue } from "./storage.js";
import { SyncStorageItem } from "../types.js";
export async function disableLoginsOnDomain(domain: string): Promise {
const currentDomains = new Set(await getDisabledDomains());
currentDomains.add(domain);
await setSyncValue(SyncStorageItem.DisabledDomains, JSON.stringify([...currentDomains]));
}
export async function getDisabledDomains(): Promise> {
const currentDomainsRaw = await getSyncValue(SyncStorageItem.DisabledDomains);
return currentDomainsRaw ? JSON.parse(currentDomainsRaw) : [];
}
export async function removeDisabledFlagForDomain(domain: string): Promise {
const currentDomains = new Set(await getDisabledDomains());
currentDomains.delete(domain);
await setSyncValue(SyncStorageItem.DisabledDomains, JSON.stringify([...currentDomains]));
}
================================================
FILE: source/background/services/entry.ts
================================================
import { SearchResult } from "buttercup";
import { createNewTab } from "../../shared/library/extension.js";
import { formatURL } from "../../shared/library/url.js";
export async function openEntryPageInNewTab(_: SearchResult, url: string): Promise {
const tab = await createNewTab(formatURL(url));
if (typeof tab?.id !== "number") {
throw new Error("No tab ID for created tab");
}
return tab.id;
}
================================================
FILE: source/background/services/init.ts
================================================
import { EventEmitter } from "eventemitter3";
import { log } from "./log.js";
import { initialise as initialiseMessaging } from "./messaging.js";
import { initialise as initialiseStorage } from "./storage.js";
import { initialise as initialiseConfig } from "./config.js";
import { generateKeys } from "./cryptoKeys.js";
import { initialise as initialiseI18n } from "../../shared/i18n/trans.js";
import { getLanguage } from "../../shared/library/i18n.js";
import { showPendingNotifications } from "./notifications.js";
enum Initialisation {
Complete = "complete",
Idle = "idle",
Running = "running"
}
const __initEE = new EventEmitter();
let __initialisation: Initialisation = Initialisation.Idle;
export async function initialise(): Promise {
if (__initialisation !== Initialisation.Idle) return;
__initialisation = Initialisation.Running;
log("initialising");
initialiseMessaging();
await initialiseStorage();
await initialiseConfig();
await initialiseI18n(getLanguage());
await generateKeys();
log("initialisation complete");
__initialisation = Initialisation.Complete;
__initEE.emit("initialised");
await showPendingNotifications();
}
export async function resetInitialisation(): Promise {
log("resetting initialisation");
__initialisation = Initialisation.Idle;
await initialise();
}
export async function waitForInitialisation(): Promise {
return new Promise((resolve) => {
if (__initialisation === Initialisation.Complete) return resolve();
__initEE.once("initialised", resolve);
});
}
================================================
FILE: source/background/services/log.ts
================================================
import { createLog } from "../../shared/library/log.js";
const LOG_NAME = "buttercup:browser:background";
let __logger: ReturnType;
export function log(...args: Array): void {
if (!__logger) {
__logger = createLog(LOG_NAME, true);
}
return __logger(...args);
}
================================================
FILE: source/background/services/loginMemory.ts
================================================
import ExpiryMap from "expiry-map";
import { searchEntriesByTerm } from "./desktop/actions.js";
import { domainsReferToSameParent, extractDomain } from "../../shared/library/domain.js";
import { UsedCredentials } from "../types.js";
interface LoginMemoryItem {
credentials: UsedCredentials;
tabID: number;
}
const LOGIN_MAX_AGE = 15 * 60 * 1000; // 15 min
let __loginMemory: ExpiryMap | null = null;
export function clearCredentials(id: string): void {
const memory = getLoginMemory();
if (memory.has(id)) {
memory.delete(id);
}
if (memory.has("last")) {
const last = memory.get("last");
if (last?.credentials.id === id) {
memory.delete("last");
}
}
}
export async function credentialsAlreadyStored(credentials: UsedCredentials): Promise {
const results = await searchEntriesByTerm(credentials.username);
const usedDomain = extractDomain(credentials.url);
return results.some((result) => {
// Check username
if (credentials.username !== result.properties.username) return false;
// Skip if search result has no URLs
if (result.urls.length <= 0) return false;
// Check if any of the domains match this one
const resultDomains = result.urls.map((url) => extractDomain(url));
if (!resultDomains.some((resDomain) => domainsReferToSameParent(resDomain, usedDomain))) {
// No matches
return false;
}
// Check if props match
return (
result.properties.username === credentials.username && result.properties.password === credentials.password
);
});
}
export function getAllCredentials(): Array {
const memory = getLoginMemory();
const credentials: Array = [];
for (const [key, item] of memory.entries()) {
if (key === "last") continue;
credentials.push(item.credentials);
}
return credentials;
}
export function getCredentialsForID(id: string): UsedCredentials | null {
const memory = getLoginMemory();
return memory.has(id) ? (memory.get(id) as LoginMemoryItem).credentials : null;
}
export function getLastCredentials(tabID: number): UsedCredentials | null {
const memory = getLoginMemory();
const last = memory.has("last") ? memory.get("last") : null;
if (!last) return null;
return last.tabID === tabID ? last.credentials : null;
}
function getLoginMemory(): ExpiryMap {
if (!__loginMemory) {
__loginMemory = new ExpiryMap(LOGIN_MAX_AGE);
}
return __loginMemory;
}
export function stopPromptForID(id: string): void {
const memory = getLoginMemory();
if (memory.has(id)) {
const existing = memory.get(id) as LoginMemoryItem;
memory.set(id, {
...existing,
credentials: {
...existing.credentials,
promptSave: false
}
});
}
const last = memory.has("last") ? memory.get("last") : null;
if (last?.credentials.id === id) {
memory.set("last", {
...last,
credentials: {
...last.credentials,
promptSave: false
}
});
}
}
export function updateUsedCredentials(credentials: UsedCredentials, tabID: number): void {
const memory = getLoginMemory();
const payload: LoginMemoryItem = {
credentials,
tabID
};
memory.set(credentials.id, payload);
memory.set("last", payload);
}
================================================
FILE: source/background/services/messaging.ts
================================================
import { Layerr } from "layerr";
import { EntryType, EntryURLType, VaultSourceID, VaultSourceStatus, getEntryURLs } from "buttercup";
import { getExtensionAPI } from "../../shared/extension.js";
import {
authenticateBrowserAccess,
getEntrySearchResults,
getOTPs,
getVaultSources,
getVaultsTree,
hasConnection,
initiateConnection,
promptSourceLock,
promptSourceUnlock,
saveExistingEntry,
saveNewEntry,
searchEntriesByTerm,
searchEntriesByURL,
testAuth
} from "./desktop/actions.js";
import { clearLocalStorage, removeLocalValue, setLocalValue } from "./storage.js";
import { errorToString } from "../../shared/library/error.js";
import {
clearCredentials,
credentialsAlreadyStored,
getAllCredentials,
getCredentialsForID,
getLastCredentials,
stopPromptForID,
updateUsedCredentials
} from "./loginMemory.js";
import { getConfig, updateConfigValue } from "./config.js";
import { disableLoginsOnDomain, getDisabledDomains, removeDisabledFlagForDomain } from "./disabledDomains.js";
import { log } from "./log.js";
import { resetInitialisation } from "./init.js";
import { getRecents, trackRecentUsage } from "./recents.js";
import { openEntryPageInNewTab } from "./entry.js";
import { getAutoLoginForTab, registerAutoLogin } from "./autoLogin.js";
import { extractDomainFromCredentials } from "../library/domain.js";
import {
BackgroundMessage,
BackgroundMessageType,
BackgroundResponse,
LocalStorageItem,
TabEventType
} from "../types.js";
import { markNotificationRead } from "./notifications.js";
import { createNewTab, getExtensionURL } from "../../shared/library/extension.js";
import { sendTabsMessage } from "./tabs.js";
async function handleMessage(
msg: BackgroundMessage,
sender: chrome.runtime.MessageSender,
sendResponse: (resp: BackgroundResponse) => void
) {
switch (msg.type) {
case BackgroundMessageType.AuthenticateDesktopConnection: {
const { code } = msg;
if (!code) {
throw new Error("No auth code provided");
}
log("complete desktop authentication");
const publicKey = await authenticateBrowserAccess(code);
await setLocalValue(LocalStorageItem.APIServerPublicKey, publicKey);
sendResponse({});
break;
}
case BackgroundMessageType.CheckDesktopConnection: {
const available = await hasConnection();
if (available) {
await testAuth();
}
sendResponse({
available
});
break;
}
case BackgroundMessageType.ClearDesktopAuthentication: {
log("clear desktop authentication");
await removeLocalValue(LocalStorageItem.APIClientID);
await removeLocalValue(LocalStorageItem.APIServerPublicKey);
sendResponse({});
break;
}
case BackgroundMessageType.ClearSavedCredentials: {
const { credentialsID } = msg;
if (!credentialsID) {
throw new Error("No credentials ID provided");
}
log(`clear saved credentials: ${credentialsID}`);
clearCredentials(credentialsID);
sendResponse({});
break;
}
case BackgroundMessageType.ClearSavedCredentialsPrompt: {
const { credentialsID } = msg;
if (!credentialsID) {
throw new Error("No credentials ID provided");
}
log(`clear saved credentials prompt: ${credentialsID}`);
stopPromptForID(credentialsID);
await sendTabsMessage({
type: TabEventType.CloseSaveDialog
});
sendResponse({});
break;
}
case BackgroundMessageType.DeleteDisabledDomains: {
const { domains } = msg;
if (!domains) {
throw new Error("No domains list provided");
}
log(`remove disabled domains: ${domains.join(", ")}`);
for (const domain of domains) {
await removeDisabledFlagForDomain(domain);
}
sendResponse({});
break;
}
case BackgroundMessageType.DisableSavePromptForCredentials: {
const { credentialsID } = msg;
if (!credentialsID) {
throw new Error("No credentials ID provided");
}
log(`disable save prompt for credentials: ${credentialsID}`);
try {
const credentials = getCredentialsForID(credentialsID);
const domain = credentials ? extractDomainFromCredentials(credentials) : null;
if (domain) {
log(`disable save prompt for domain: ${domain}`);
await disableLoginsOnDomain(domain);
}
} catch (err) {
throw new Layerr(err, "Failed disabling save prompt for domain");
}
await sendTabsMessage({
type: TabEventType.CloseSaveDialog
});
sendResponse({});
break;
}
case BackgroundMessageType.GetAutoLoginForTab: {
const tabID = sender.tab?.id;
if (!tabID) {
sendResponse({ autoLogin: null });
break;
}
const entry = getAutoLoginForTab(tabID);
sendResponse({ autoLogin: entry });
break;
}
case BackgroundMessageType.GetConfiguration: {
const config = getConfig();
sendResponse({
config
});
break;
}
case BackgroundMessageType.GetDesktopVaultSources: {
const sources = await getVaultSources();
sendResponse({
vaultSources: sources
});
break;
}
case BackgroundMessageType.GetDesktopVaultsTree: {
const tree = await getVaultsTree();
sendResponse({
vaultsTree: tree
});
break;
}
case BackgroundMessageType.GetDisabledDomains: {
const domains = await getDisabledDomains();
sendResponse({
domains
});
break;
}
case BackgroundMessageType.GetLastSavedCredentials: {
const { excludeSaved = false } = msg;
const tabID = sender.tab?.id;
if (!tabID) {
sendResponse({ credentials: [null] });
break;
}
let credentials = getLastCredentials(tabID);
if (credentials && excludeSaved && (await credentialsAlreadyStored(credentials))) {
credentials = null;
}
sendResponse({
credentials: [credentials]
});
break;
}
case BackgroundMessageType.GetOTPs: {
const otps = await getOTPs();
sendResponse({
otps
});
break;
}
case BackgroundMessageType.GetRecentEntries: {
const { count = 10 } = msg;
const sources = await getVaultSources();
const unlockedIDs: Array = sources.reduce((output, source) => {
if (source.state === VaultSourceStatus.Unlocked) {
return [...output, source.id];
}
return output;
}, []);
const recentItems = await getRecents(unlockedIDs);
recentItems.splice(count, Infinity);
const searchResults = await getEntrySearchResults(
recentItems.map((item) => ({
entryID: item.entryID,
sourceID: item.sourceID
}))
);
sendResponse({
searchResults
});
break;
}
case BackgroundMessageType.GetSavedCredentials: {
const credentials = getAllCredentials();
sendResponse({
credentials
});
break;
}
case BackgroundMessageType.GetSavedCredentialsForID: {
const { credentialsID, excludeSaved = false } = msg;
if (!credentialsID) {
throw new Error("No credentials ID provided");
}
let credentials = getCredentialsForID(credentialsID);
if (credentials && excludeSaved && (await credentialsAlreadyStored(credentials))) {
credentials = null;
}
sendResponse({
credentials: [credentials]
});
break;
}
case BackgroundMessageType.InitiateDesktopConnection: {
log("start desktop authentication");
await initiateConnection();
await createNewTab(getExtensionURL("full.html#/connect"));
sendResponse({});
break;
}
case BackgroundMessageType.MarkNotificationRead: {
const { notification } = msg;
if (!notification) {
throw new Error("No notification provided");
}
log(`mark notification read: ${notification}`);
await markNotificationRead(notification);
sendResponse({});
break;
}
case BackgroundMessageType.OpenEntryPage: {
const { autoLogin, entry } = msg;
if (!entry) {
throw new Error("No entry provided");
}
const [url = null] = getEntryURLs(entry.properties, EntryURLType.Login);
if (!url) {
sendResponse({ opened: false });
return;
}
log(`open entry page by url: ${entry.id} (${url})`);
const tabID = await openEntryPageInNewTab(entry, url);
if (autoLogin) {
registerAutoLogin(entry, tabID);
}
sendResponse({ opened: true });
break;
}
case BackgroundMessageType.OpenSaveCredentialsPage: {
await createNewTab(getExtensionURL("full.html#/save-credentials"));
await sendTabsMessage({
type: TabEventType.CloseSaveDialog
});
sendResponse({});
break;
}
case BackgroundMessageType.PromptLockSource: {
const { sourceID } = msg;
if (!sourceID) {
throw new Error("No source ID provided");
}
log(`request lock source: ${sourceID}`);
const locked = await promptSourceLock(sourceID);
sendResponse({
locked
});
break;
}
case BackgroundMessageType.PromptUnlockSource: {
const { sourceID } = msg;
if (!sourceID) {
throw new Error("No source ID provided");
}
log(`request unlock source: ${sourceID}`);
await promptSourceUnlock(sourceID);
sendResponse({});
break;
}
case BackgroundMessageType.ResetSettings: {
log(`reset settings`);
await clearLocalStorage();
await resetInitialisation();
sendResponse({});
break;
}
case BackgroundMessageType.SaveCredentialsToVault: {
const { sourceID, groupID, entryID = null, entryProperties, entryType = EntryType.Website } = msg;
if (!sourceID) {
throw new Error("No source ID provided");
}
if (!groupID) {
throw new Error("No group ID provided");
}
if (!entryProperties) {
throw new Error("No entry properties provided");
}
if (entryID) {
log(`save credentials to existing entry: ${entryID} (source=${sourceID})`);
await saveExistingEntry(sourceID, groupID, entryID, entryProperties);
sendResponse({
entryID: null
});
} else {
log(`save credentials to new entry (source=${sourceID})`);
const entryID = await saveNewEntry(sourceID, groupID, entryType, entryProperties);
sendResponse({
entryID
});
}
break;
}
case BackgroundMessageType.SaveUsedCredentials: {
const { credentials } = msg;
if (!credentials) {
throw new Error("No source ID provided");
}
if (!sender.tab?.id) {
throw new Error("No tab ID available for background message");
}
updateUsedCredentials(credentials, sender.tab.id);
sendResponse({});
break;
}
case BackgroundMessageType.SearchEntriesByTerm: {
const { searchTerm } = msg;
if (!searchTerm) {
throw new Error("No search term provided");
}
const searchResults = await searchEntriesByTerm(searchTerm);
sendResponse({
searchResults
});
break;
}
case BackgroundMessageType.SearchEntriesByURL: {
const { url } = msg;
if (!url) {
throw new Error("No URL provided");
}
const searchResults = await searchEntriesByURL(url);
sendResponse({
searchResults
});
break;
}
case BackgroundMessageType.SetConfigurationValue: {
const { configKey, configValue } = msg;
if (!configKey || typeof configValue === "undefined") {
throw new Error("Invalid configuration proivided provided");
}
await updateConfigValue(configKey, configValue);
sendResponse({});
break;
}
case BackgroundMessageType.TrackRecentEntry: {
const { entry } = msg;
if (!entry) {
throw new Error("No entry provided");
}
if (!entry.sourceID) {
throw new Error(`No source ID in entry result: ${entry.id}`);
}
await trackRecentUsage(entry.sourceID, entry.id);
sendResponse({});
break;
}
default:
throw new Layerr(`Unrecognised message type: ${(msg as any).type}`);
}
}
export function initialise() {
getExtensionAPI().runtime.onMessage.addListener((request, sender, sendResponse) => {
handleMessage(request, sender, sendResponse).catch((err) => {
console.error(err);
sendResponse({
error: errorToString(new Layerr(err, "Background task failed"))
});
});
return true;
});
}
================================================
FILE: source/background/services/notifications.ts
================================================
import { getSyncValue, setSyncValue } from "./storage.js";
import { SyncStorageItem } from "../types.js";
import { NOTIFICATION_NAMES } from "../../shared/notifications/index.js";
import { createNewTab, getExtensionURL } from "../../shared/library/extension.js";
async function getPendingNotifications(): Promise> {
const existingNotificationsRaw = await getSyncValue(SyncStorageItem.Notifications);
const existingNotifications = existingNotificationsRaw ? existingNotificationsRaw.split(",") : [];
return NOTIFICATION_NAMES.filter((name) => !existingNotifications.includes(name));
}
export async function markNotificationRead(name: string): Promise {
const existingNotificationsRaw = await getSyncValue(SyncStorageItem.Notifications);
const notifications = existingNotificationsRaw ? existingNotificationsRaw.split(",") : [];
if (!notifications.includes(name)) {
notifications.push(name);
}
await setSyncValue(SyncStorageItem.Notifications, notifications.join(","));
}
export async function showPendingNotifications(): Promise {
const notifications = await getPendingNotifications();
if (notifications.length <= 0) return;
await createNewTab(getExtensionURL(`full.html#/notifications?notifications=${notifications.join(",")}`));
}
================================================
FILE: source/background/services/recents.ts
================================================
import { EntryID, VaultSourceID } from "buttercup";
import { ChannelQueue, TaskPriority } from "@buttercup/channel-queue";
import ms from "ms";
import { getSyncValue, setSyncValue } from "./storage.js";
import { SyncStorageItem } from "../types.js";
export interface RecentItem {
entryID: EntryID;
sourceID: VaultSourceID;
uses: Array;
}
const MAX_USE_AGE = ms("30d");
let __queue: ChannelQueue | null = null;
function getQueue(): ChannelQueue {
if (!__queue) {
__queue = new ChannelQueue();
}
return __queue;
}
export async function getRecents(sourceIDs: Array): Promise> {
const currentRecentsRaw = await getSyncValue(SyncStorageItem.RecentItems);
if (!currentRecentsRaw) return [];
const currentRecents = JSON.parse(currentRecentsRaw) as Array;
return currentRecents.filter((recent) => sourceIDs.includes(recent.sourceID));
}
function sortUses(itemA: RecentItem, itemB: RecentItem): number {
if (itemA.uses.length > itemB.uses.length) return -1;
if (itemB.uses.length > itemA.uses.length) return 1;
return 0;
}
function stripOldUses(items: Array): Array {
const earliestTs = Date.now() - MAX_USE_AGE;
return items.reduce((output: Array, item: RecentItem) => {
const hasRecentUse = item.uses.some((ts) => ts >= earliestTs);
if (!hasRecentUse) return output;
return [
...output,
{
...item,
uses: item.uses.filter((ts) => ts >= earliestTs)
}
];
}, []);
}
export async function trackRecentUsage(sourceID: VaultSourceID, entryID: EntryID): Promise {
const channel = getQueue().channel("write");
await channel.enqueue(
async () => {
const currentRecentsRaw = await getSyncValue(SyncStorageItem.RecentItems);
let currentRecents = currentRecentsRaw ? (JSON.parse(currentRecentsRaw) as Array) : [];
let existingResult = currentRecents.find(
(recent) => recent.sourceID === sourceID && recent.entryID === entryID
);
if (existingResult) {
existingResult.uses.unshift(Date.now());
} else {
existingResult = {
entryID,
sourceID,
uses: [Date.now()]
};
currentRecents.push(existingResult);
}
currentRecents = stripOldUses(currentRecents);
currentRecents.sort(sortUses);
await setSyncValue(SyncStorageItem.RecentItems, JSON.stringify(currentRecents));
},
TaskPriority.Normal,
`${sourceID}-${entryID}`
);
}
================================================
FILE: source/background/services/storage/BrowserStorageInterface.ts
================================================
import { StorageInterface } from "buttercup";
import { getExtensionAPI } from "../../../shared/extension.js";
export function getSyncStorage() {
return getExtensionAPI().storage.sync;
}
export function getNonSyncStorage() {
return getExtensionAPI().storage.local;
}
export class BrowserStorageInterface extends StorageInterface {
protected _storage: chrome.storage.StorageArea;
constructor(storage: chrome.storage.StorageArea = getSyncStorage()) {
super();
this._storage = storage;
}
get storage() {
return this._storage;
}
async getAllKeys() {
return new Promise>((resolve) => {
this.storage.get(null, (allItems) => {
resolve(Object.keys(allItems));
});
});
}
async getValue(name: string) {
return new Promise((resolve) => {
this.storage.get(name, (items) => {
resolve(items[name]);
});
});
}
async removeKey(name: string) {
return new Promise((resolve) => {
this.storage.remove(name, () => resolve());
});
}
async setValue(name: string, value: any) {
return new Promise((resolve) => {
this.storage.set({ [name]: value }, () => resolve());
});
}
}
================================================
FILE: source/background/services/storage.ts
================================================
import { log } from "./log.js";
import { BrowserStorageInterface, getNonSyncStorage, getSyncStorage } from "./storage/BrowserStorageInterface.js";
import { LocalStorageItem, SyncStorageItem } from "../types.js";
const VALID_LOCAL_KEYS = Object.values(LocalStorageItem);
const VALID_SYNC_KEYS = Object.values(SyncStorageItem);
export async function clearLocalStorage(): Promise {
const localStorage = getLocalStorage();
const syncStorage = getSynchronisedStorage();
const keys = await localStorage.getAllKeys();
for (const key of keys) {
log(`clearing local storage key: ${key}`);
await localStorage.removeKey(key);
}
await syncStorage.removeKey(SyncStorageItem.Notifications);
}
function getLocalStorage(): BrowserStorageInterface {
return new BrowserStorageInterface(getNonSyncStorage());
}
export async function getLocalValue(key: LocalStorageItem): Promise {
return getLocalStorage().getValue(key) ?? null;
}
export async function getSyncValue(key: SyncStorageItem): Promise {
return getSynchronisedStorage().getValue(key) ?? null;
}
function getSynchronisedStorage(): BrowserStorageInterface {
return new BrowserStorageInterface(getSyncStorage());
}
export async function initialise() {
const localStorage = getLocalStorage();
{
const keys = await localStorage.getAllKeys();
for (const key of keys) {
const valid = VALID_LOCAL_KEYS.find((local) => key === local || key.indexOf(local) === 0);
if (!valid) {
log(`remove unrecognised local storage key: ${key}`);
await localStorage.removeKey(key);
}
}
}
const syncStorage = getSynchronisedStorage();
{
const keys = await syncStorage.getAllKeys();
for (const key of keys) {
const valid = VALID_SYNC_KEYS.find((local) => key === local || key.indexOf(local) === 0);
if (!valid) {
log(`remove unrecognised sync storage key: ${key}`);
await syncStorage.removeKey(key);
}
}
}
}
export async function removeLocalValue(key: LocalStorageItem): Promise {
await getLocalStorage().removeKey(key);
}
export async function removeSyncValue(key: SyncStorageItem): Promise {
await getSynchronisedStorage().removeKey(key);
}
export async function setLocalValue(key: LocalStorageItem, value: string): Promise {
return getLocalStorage().setValue(key, value);
}
export async function setSyncValue(key: SyncStorageItem, value: string): Promise {
return getSynchronisedStorage().setValue(key, value);
}
================================================
FILE: source/background/services/tabs.ts
================================================
import { getExtensionAPI } from "../../shared/extension.js";
import { TabEvent } from "../types.js";
export async function sendTabsMessage(payload: TabEvent, tabIDs: Array | null = null): Promise {
const browser = getExtensionAPI();
const targetTabIDs = Array.isArray(tabIDs)
? tabIDs
: (
await browser.tabs.query({
status: "complete"
})
).reduce((output: Array, tab) => {
if (!tab.id) return output;
return [...output, tab.id];
}, []);
await Promise.all(
targetTabIDs.map(async (tabID) => {
browser.tabs.sendMessage(tabID, payload);
})
);
}
================================================
FILE: source/background/types.ts
================================================
export * from "../shared/types.js";
export enum LocalStorageItem {
APIClientID = "bcup:api:clientID",
APIPrivateKey = "bcup:api:privateKey",
APIPublicKey = "bcup:api:publicKey",
APIServerPublicKey = "bcup:api:serverPublicKey"
}
export enum SyncStorageItem {
Configuration = "bcup:configuration",
DisabledDomains = "bcup:disabledDomains",
Notifications = "bcup:notifications",
RecentItems = "bcup:recents"
}
================================================
FILE: source/full/applications/full.tsx
================================================
import React from "react";
import ReactDOM from "react-dom";
import { App } from "../components/App.js";
import { initialise } from "../services/init.js";
initialise()
.then(() => {
ReactDOM.render(
,
document.getElementById("root"),
);
})
.catch(err => {
console.error(err);
});
================================================
FILE: source/full/components/App.tsx
================================================
import React from "react";
import {
createHashRouter,
RouterProvider
} from "react-router-dom";
import { ThemeProvider } from "../../shared/components/ThemeProvider.jsx";
import { ConnectPage } from "./pages/connect/index.jsx";
import { AttributionsPage } from "./pages/AttributionsPage.jsx";
import { SaveCredentialsPage } from "./pages/saveCredentials/index.js";
import { useBodyThemeClass, useTheme } from "../../shared/hooks/theme.js";
import { RouteError } from "../../shared/components/RouteError.js";
import { DisabledDomainsPage } from "./pages/DisabledDomainsPage.js";
import { NotificationsPage } from "./pages/NotificationsPage.js";
const ROUTER = createHashRouter([
{
path: "/connect",
element: ,
errorElement:
},
{
path: "/attributions",
element: ,
errorElement:
},
{
path: "/disabled-domains",
element: ,
errorElement:
},
{
path: "/notifications",
element: ,
errorElement: ,
loader: ({ request }) => {
const url = new URL(request.url);
const notifications = url.searchParams.get("notifications");
return { notifications };
}
},
{
path: "/save-credentials",
element: ,
errorElement:
}
]);
export function App() {
const theme = useTheme();
useBodyThemeClass(theme);
return (
);
}
================================================
FILE: source/full/components/Layout.tsx
================================================
import React from "react";
import styled from "styled-components";
import { Divider } from "@blueprintjs/core";
import { ChildElements } from "../types.js";
import BUTTERCUP_LOGO from "../../../resources/buttercup-128.png";
interface LayoutProps {
children: ChildElements;
title: string;
}
const ContentContainer = styled.div`
padding: 1rem;
`;
const Header = styled.div`
margin: 0.5rem 1rem 0;
padding: 0.3rem 0;
display: flex;
justify-content: flex-start;
align-items: center;
`;
const MainContent = styled.div`
width: 100vw;
min-height: 100vh;
padding: 3rem 0;
`;
const Title = styled.h1`
margin: 0px 0px 4px 0px;
padding: 0;
font-size: 18px;
flex: 1;
`;
const TitleImage = styled.img`
width: 28px;
height: 28px;
margin-bottom: 3px;
margin-right: 6px;
`;
const Wrapper = styled.div`
width: 680px;
margin: 0 auto;
display: flex;
flex-direction: column;
@media screen and (max-width: 700px) {
width: 100%;
}
`;
export function Layout({ children, title }: LayoutProps) {
return (
{title}{children}
);
}
================================================
FILE: source/full/components/pages/AttributionsPage.tsx
================================================
import React from "react";
import styled from "styled-components";
import { Layout } from "../Layout.js";
import { t } from "../../../shared/i18n/trans.js";
import { useTitle } from "../../hooks/document.js";
import COMPUTER_ICON from "../../../../resources/providers/local-256.png";
const AttributionLI = styled.li`
display: flex;
flex-direction: row;
align-items: center;
`;
const ImageIcon = styled.img`
width: auto;
height: 32px;
margin-right: 12px;
`;
export function AttributionsPage() {
useTitle(t("attributions-page.title"));
return (
Buttercup is Open Source Software and makes use of many free and openly available libraries and resources.
Below are a list of resource attributions that this browser extension makes use of.