1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-25 12:15:18 +01:00

Individual Vault Item Encryption Feature (#6241)

* PM-1049 - TODO: replace base component with business service

* updated server version

* disabled cipher key encryption

* add new storage to replace MasterKey with UserSymKey

* add storage for master key encrypted user symmetric key

* Begin refactor of crypto service to support new key structure

* remove provided key from getKeyForUserEncryption

* add decryption with MasterKey method to crypto service

* update makeKeyPair on crypto service to be generic

* add type to parameter of setUserKey in abstraction of crypto service

* add setUserSymKeyMasterKey so we can set the encrypted user sym key from server

* update cli with new crypto service methods
- decrypt user sym key and set when unlocking

* separate the user key in memory from user keys in storage

* add new memory concept to crypto service calls in cli

* update auth service to use new crypto service

* update register component in lib to use new crypto service

* update register component again with more crypto service

* update sync service to use new crypto service methods

* update send service to use new crypto service methods

* update folder service to use new crypto service methods

* update cipher service to use new crypto service

* update password generation service to use new crypto service

* update vault timeout service with new crypto service

* update collection service to use new crypto service

* update emergency access components to use new crypto service methods

* migrate login strategies to new key model
- decrypt and set user symmetric key if Master Key is available
- rename keys where applicable
- update unit tests

* migrate pin to use user's symmetric key instead of master key
- set up new state
- migrate on lock component
- use new crypto service methods

* update pin key when the user symmetric key is set
- always set the protected pin so we can recreate pin key from user symmetric key
- stop using EncryptionPair in account
- use EncString for both pin key storage
- update migration from old strategy on lock component

* set user symmetric key on lock component
- add missed key suffix types to crypto service methods

* migrate auto key
- add helper to internal crypto service method to migrate

* remove additional keys in state service clean

* clean up the old pin keys in more flows
- in the case that the app is updated while logged in and the user changes their pin, this will clear the old pin keys

* finish migrate auto key if needed
- migrate whenever retrieved from storage
- add back the user symmetric key toggle

* migrate biometrics key
- migrate only on retrieval

* fix crypto calls for key connector and vault timeout settings

* update change password components with new crypto service

* update assortment of leftover old crypto service calls

* update device-crypto service with new crypto service

* remove old EncKey methods from crypto service

* remove clearEncKey from crypto service

* move crypto service jsdoc to abstraction

* add org key type and new method to build a data enc key for orgs

* fix typing of bulk confirm component

* fix EncString serialization issues & various fixes

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* update account model with new keys serialization

* migrate native messaging for biometrics to use new key model
- support backwards compatibility
- update safari web extension to send user key
- add error handling

* add early exit to native messaging flow for errors

* improve error strings in crypto service

* disable disk cache for browser due to bg script/popup race conditions

* clear bio key when pin is migrated as bio is refreshed

* share disk cache to fix syncing issues between contexts

* check for ephemeral pin before process reload

* remove state no longer needed and add JSDOC

* fix linter

* add new types to tests

* remove cryptoMasterKeyB64 from account

* fix tests imports

* use master key for device approvals still

* cleanup old TODOs, add missing crypto service parameters

* fix cli crypto service calls

* share disk cache between contexts on browser

* Revert "share disk cache between contexts on browser"

This reverts commit 56a590c491.

* use user sym key for account changing unlock verification

* PM-1565 Added item key property to cipher export domain (#5580)

* PM-1565 Added item key property to cipher export domain

* enabled cipher key encryption

* Updated getCipherKeyEncryptionEnabled validation to also return true if the serverVersion matches the minVersion

* Using async/await when getting decrypted ciphers on getOrganizationDecryptedExport

* Disabling CipherKey

* add tests to crypto service

* rename 'user symmetric key' with 'user key'

* remove userId from browser crypto service

* updated EncKey to UserKey where applicable

* jsdoc deprecate account properties

* use encrypt service in crypto service

* use encrypt service in crypto service

* require key in validateUserKey

* check storage for user key if missing in memory

* change isPinLockSet to union type

* move biometric check to electron crypto service

* add secondary fallback name for bio key for safari

* migrate master key if found

* pass key to encrypt service

* rename pinLock to pinEnabled

* use org key or user key for encrypting attachments

* refactor makeShareKey to be more clear its for orgs

* rename retrieveUserKeyFromStorage

* clear deprecated keys when setting new user key

* fix cipher service test

* options is nullable while setting user key

* Reordering Service creation on cli's bw.ts to fix ConfigApiService (#5684)

* more crypto service refactors
- check for auto key when getting user key
- consolidate getUserKeyFromMemory and FromStorage methods
- move bio key references out of base crypto service
- update either pin key when setting user key instead of lock component
- group deprecated methods
- rename key legacy method

* Feature/PM-1049 - TDEFflow 3 login decryption options - PR feedback changes (#5642)

* PM-1049 - PR Feedback change - Browser - replace incorrect use of routerlink with manual attribute styling to keep anchor styling + tab focus while not having a router action race condition for the log out action to complete.

* PM-1049 - PR Feedback - State Service changes - rename get/setAcctDecryptionOptions to  get/setAccountDecryptionOptions

* PM-1049 - PR Feedback changes - LoginDecryptionOptionsComp - Remove unncessary appA11yTitle directives as title / aria text would be identical to the displayed inner button text.

* DeviceType - Create sets of device types which other components can reference to avoid having to manually define groups of device types.

* PM-1049 - PR Feedback Changes - Update base-login-decryption-options component to leverage async piped observables per best practices. Updated all client templates to leverage new data streams.

* PM-1049 - BaseLoginDecryptionOptionsComp - Add validation service for generic error handling

* PM-1049 - DeviceResponse mistakenly had name as a number instead of a string

* PM-1049 - First draft of creating observable based data store service for Devices so that the base login comp can leverage it instead of calling the devices API service directly (as it will be moved into the SDK in the future).

* PM-1049 - Register new DevicesService on jslib-services module for use in components.

* PM-1049 - Add new hasDevicesOfTypes call to devices data store svc + devices API service.

* PM-1049 - BaseLoginDecryptionOptionsComp - wire up call to devicesService.hasDevicesOfTypes to replace getDevices() to avoid bringing down all trusted device information unnecessarily.

* PM-1049 - LoginDecryptionOptionsComp - Web HTML - clean up loading state so it displays spinner centered properly.

* PM-1049 - LoginDecryptionOptionsComp - Desktop HTML - Don't show login initiated title while page is loading to match other clients behavior.

* PM-1049 - Devices Services - Update naming of hasDevicesOfTypes to match new name on back end + route change to getDevicesExistenseByTypes

* PM-1049 - Device Response & View models - remove keys which are going to be deprecated on the base model

* PM-1049 - DevicesService - devicesBSubject --> devicesSubject rename per PR feedback

* PM-1049 - Devices Services - correct spelling of existence (*facepalm*)

* PM-1049 - Update comment for clarity per PR feedback

* PM-1049 - DevicesSvc - UserSymKey --> UserKey rename

* PM-1049 - BaseLoginDecryptionOptions - replace user email source - get from stateService vs tokenService.

* PM-1049 - BaseLoginDecryptionOptions - Remove uncessary check for userEmail as we will always have it here otherwise everything in the app is broken.

* PM-1049 - BaseLoginDecryptionOptions - Finish cleaning up removal of user email from showReqAdminApprovalBtn$ stream

* PM-1049 - LoginDecryptionOptionsComp - HTML revisions in web & browser to better space out buttons using tailwind or top margin to avoid need for multiple async pipes and shareReplay.

* PM-1049 - DevicesService - of course all observables should have $ suffix. Facepalm.

* PM-1049 - BaseLoginDecryptionOptionsComp - Update verbiage and style of destroy observable used for hooking into ngOnDestroy lifecycle to clean up all observables

* PM-1049 - BaseLoginDecryptionOptions - PR feedback changes - refactor user email to have an underlying bSubject stream to ensure subscription/promise execution separately from the template async pipe subscribing to the stream.

* PM-1049 - DevicesApiService - getDevicesExistenceByTypes - PR feedback - explicitly convert result to boolean instead of casting.

* PM-1049 - BaseLoginDecryptionOptionsComp - Add ShareReplay for getAccountDecryptionOptions + context per PR feedback

* PM-1049 - LoginDecryptionOptionsComp - Completely back away from template async pipe reactive approach as it caused massively increased complexity for little gain. Instead, just focus on reactively pulling asynchronously retrieved data and setting page loading state simply. This just works and is so much less overhead. + Add comments re flows of the component to be done later

* PM-1049- Revert DevicesService implementation from smart data store cache service giant mess into simple, clean data passthrough service to avoid complexity and keep moving forward. YAGNI

Co-authored-by: Andreas Coroiu <andreas@andreascoroiu.com>

* PM-1049 -  DeviceCryptoService - Add decryptUserKey method (WIP)

* PM-1049 - AccountDecryptionOptions - add get helpers for checking for trusted device / key connector decryption option existence.

* PM-1049 - SSO Login Strategy - added comments in setUserKey method for where we will probably be consuming device keys and determining if the device is trusted or not (i.e., if we can get a decrypted user sym key in memory)

* PM-1049 - DeviceCryptoSvc.decryptUserKey - Update method to properly use state service device key retrieval + add TODO to figure out what to do if user has previously had a device key and has cleared their local cache (which will result in the device being untrusted now)

* PM-1049 - SSO Login Strategy - add comment re future passkey login strategy support

* PM-2759 - SSO & 2FA components updated with v0 of navigation logic to send users to LoginDecryptionOptions

* PM-1049 - Account > AccountDecryptionOptions - can't create getter helper methods for determining if user has decryption options b/c of issues w/ account deserialization. Moving past b/c I can just easily check if the given options are not undefined.

* PM-2759 - Add TODOs for deprecation of id token response resetMasterPassword logic and replacement with use of accountDecryptionOptions

---------

Co-authored-by: Andreas Coroiu <andreas@andreascoroiu.com>

* PM-2582 Fix adding attachments (#5692)

* revert sharing disk cache between contexts

* fix tests

* PM-2791 Reordered service creation (#5701)

* Turned off flag in production.json

* add better tests to crypto service

* add hack to get around duplicate instances of disk cache on browser

* prevent duplicate cache deletes in browser

* fix browser state service tests

* Feature/PM-1212 - TDE - Approve with master password flow (#5706)

* PM-1212 - StateSvc - Add getUserDeviceTrustChoice && setUserDeviceTrustChoice to persist user's choice in local storage in case of refresh on login approval screens (ex: lock)

* PM-1212 - DeviceCryptoSvc - Add getUserDeviceTrustChoice && setUserDeviceTrustChoice as state service is lower level service for caching

* PM-1212 - LoginDecryptionOptionsComp - Save result of rememberEmail checkbox into local storage via deviceCryptoService.setUserDeviceTrustChoice

* PM-1212 - Lock component - after user key is set, check if user chose to establish trust, and if they did, then establish trust and reset choice.

* PM-1212 - Update naming of methods per discussion with Jake + add comment explaining intended single use retrieval and need for resetting the value.

* DeviceCryptoService - Refactor - decryptUserKey --> decryptUserKeyWithDeviceKey to match crypto service refactor naming convention

* PM-1212 - Refactor State Service per PR feedback to store trustDeviceChoiceForDecryption on Account.settings b/c the temp setting is scoped to a user.

* PM-2759 - SSO & 2FA Navigation to TDE Comp - Needs more work - Found scenarios on web with 2FA in which the expected navigation doesn't work. Adding TODO to assist in fixing

* (1) Add Trust to DeviceCryptoService name
(2) Move DeviceTrustCryptoService under auth folder

* PM-1212 - Add tests for new getUserTrustDeviceChoiceForDecryption and setUserTrustDeviceChoiceForDecryption methods + TODOs for future tests.

* PM-1212- Renaming / moving DeviceTrustCryptoService broke all the things - fixed all the client builds.

* PM-1212- Copy doc comment to abstraction per PR feedback

* PM-1212 - BaseLoginDecryptionOptions comp - remove unncessary cast to form control as apparently reactive forms now properly derives types.

* [PM-1203] Replace MP confirmation with verification code (#5656)

* [PM-1203] feat: ask for OTP if user does not have MP

* [PM-1203] feat: add backwards compatibility for accounts/servers without decryption options

* [PM-1203] feat: move hasMasterPassword to user-verification.service

* [PM-1203] fix: remove duplicate implementation from crypto service

* [PM-1203] fix: cli build

* Tweak device trust crypto service implementation to match mobile late… (#5744)

* Tweak device trust crypto service implementation to match mobile latest which results in more single responsibility methods

* Update tests to match device trust crypto service implementation changes

* update comment about state service

* update pinLockType states and add jsdocs

* add missed pinLockType changes

* [PM-1033] Org invite user creation flow 1 (#5611)

* [PM-1033] feat: basic redirection to login initiated

* [PM-1033] feat: add ui for TDE enrollment

* [PM-1033] feat: implement auto-enroll

* [PM-1033] chore: add todo

* [PM-1033] feat: add support in browser

* [PM-1033] feat: add support for desktop

* [PM-1033] feat: improve key check hack to allow regular accounts

* [PM-1033] feat: init asymmetric account keys

* [PM-1033] chore: temporary fix bug from merge

* [PM-1033] feat: properly check if user can go ahead an auto-enroll

* [PM-1033] feat: simplify approval required

* [PM-1033] feat: rewrite using discrete states

* [PM-1033] fix: clean-up and fix merge artifacts

* [PM-1033] chore: clean up empty ng-container

* [PM-1033] fix: new user identification logic

* [PM-1033] feat: optimize data fetching

* [PM-1033] feat: split user creating and reset enrollment

* [PM-1033] fix: add missing loading false statement

* [PM-1033] fix: navigation logic in sso component

* [PM-1033] fix: add missing query param

* [PM-1033] chore: rename to `ExistingUserUntrustedDevice`

* PM-1033 - fix component templates to reference `ExistingUserUntrustedDevice` so clients can build

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>

* remove extra partial key

* set master key on lock component

* rename key hash to password hash on crypto service

* fix cli

* rename enc user key setter in crypto service

* Adds Events & Human Readable Messages (#5746)

* [PM-1202] Hide the Master Password tab on Settings / Security (#5649)

* [PM-1203] feat: ask for OTP if user does not have MP

* [PM-1203] feat: get master password status from decryption options

* [PM-1203] feat: add backwards compatibility for accounts/servers without decryption options

* [PM-1203] feat: move hasMasterPassword to user-verification.service

* fix merge issues

* Change getUserTrustDeviceChoiceForDecryption / setUserTrustDeviceChoiceForDecryption to getShouldTrustDevice / setShouldTrustDevice (#5795)

* Auth/[PM-1260] - Existing User - Login with Trusted Device (Flow 2) (#5775)

* PM-1378 - Refactor - StateSvc.getDeviceKey() must actually convert JSON obj into instance of SymmetricCryptoKey

* TODO: BaseLoginDecryptionOptionsComponent - verify new user check doesn't improperly pick up key connector users

* PM-1260 - Add new encrypted keys to TrustedDeviceUserDecryptionOptionResponse

* PM-1260 - DeviceTrustCryptoSvc - decryptUserKeyWithDeviceKey: (1) update method to optionally accept deviceKey (2) Return null user key when no device key exists (3) decryption of user key now works in the happy path

* PM-1260 - LoginStrategy - SaveAcctInfo - Must persist device key on new account entity created from IdTokenResponse for TDE to work

* PM-1260 - SSO Login Strategy - setUserKey refactor - (1) Refactor existing logic into trySetUserKeyForKeyConnector + setUserKeyMasterKey call and (2) new trySetUserKeyWithDeviceKey method for TDE

* PM-1260 - Refactor DeviceTrustCryptoService.decryptUserKeyWithDeviceKey(...) - Add try catch around decryption attempts which removes device key (and trust) on decryption failure + warn.

* PM-1260 - Account - Add deviceKey to fromJSON

* TODO: add device key tests to account keys

* TODO: figure out state service issues with getDeviceKey or if they are an issue w/ the account deserialization as a whole

* PM-1260 - Add test suite for decryptUserKeyWithDeviceKey

* PM-1260 - Add interfaces for server responses for UserDecryptionOptions to make testing easier without having to use the dreaded any type.

* PM-1260 - SSOLoginStrategy - SetUserKey - Add check looking for key connector url on user decryption options + comment about future deprecation of tokenResponse.keyConnectorUrl

* PM-1260 - SSO Login Strategy Spec file - Add test suite for TDE set user key logic

* PM-1260 - BaseLoginStrategy - add test to verify device key persists on login

* PM-1260 - StateService - verified that settings persist properly post SSO and it's just device keys we must manually instantiate into SymmetricCryptoKeys

* PM-1260 - Remove comment about being unable to feature flag auth service / login strategy code due to circ deps as we don't need to worry about it b/c of the way we've written the new logic to be additive.

* PM-1260 - DevicesApiServiceImplementation - Update constructor to properly use abstraction for API service

* PM-1260 - Browser - AuthService - (1) Add new, required service factories for auth svc and (2) Update auth svc creation in main.background with new deps

* PM-1260 - CLI - Update AuthSvc deps

* PM-1260 - Address PR feedback to add clarity / match conventions

* PM-1260 - Resolving more minor PR feedback

* PM-1260 - DeviceTrustCryptoService - remove debug warn

* PM-1378 - DeviceTrustCryptoSvc - TrustDevice - Fix bug where we only partially encrypted the user key with the device public key b/c I incorrectly passed userKey.encKey (32 bytes) instead of userKey.key (64 bytes) to the rsaEncrypt function which lead to an encryption type mismatch when decrypting the user's private key with the 32 byte decrypted user key obtained after TDE login.  (Updated happy path test to prevent this from happening again)

* PM-1260 - AccountKeys tests - add tests for deviceKey persistence and deserialization

* PM-1260 - DeviceTrustCryptoSvc Test - tweak verbiage per feedback

* PM-1260 - DeviceTrustCryptoSvc - Test verbiage tweak part 2

* Update apps/browser/src/background/service-factories/devices-api-service.factory.ts

per PR feedback

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Defect - LockComp - After setting user key, must AWAIT retrieval of user's previous choice to have trusted the device or not. (#5804)

* [PM-2928] [PM-2929] [PM-2930] Fixes for: [PM-1203] Replace MP confirmation with verification code (#5798)

* [PM-2928] feat: hide change email if user doen't have MP

* [PM-2929] feat: hide KDF settings if user doesn't have MP

* [PM-2930] feat: remove MP copy

* Removed self-hosted check from TDE SSO config. (#5837)

* [PM-2998] Move Approving Device Check (#5822)

* Switch to retrieving approving device from token response

- Remove exist-by-types API call
- Define `HasApprovingDevices` on TDE options

* Update Naming

* Update Test

* Update Missing Names

* [PM-2908] feat: show account created toast (#5810)

* fix bug where we weren't passing MP on Restart to migrate method in lock

* fix: buffer null error (#5856)

* Auth/[pm-2759] - TDE - SSO and 2FA routing logic (#5829)

* PM-2759 - SsoComp - (1) Temp remove all TDE routing logic (2) Refactor existing navigation logic via new component utility function navigateViaCallbackOrRoute

* PM-2759 - SSO Component - Create test suite for logIn logic

* PM-2759 - SsoComp Tests - add disclaimer regarding testing private methods and props

* PM-1259 - SSO Comp - Refactor LogIn method to use functions for each navigation case for improved readability

* PM-1259 - SSO Comp Tests - Add tests for error case during login + test for new handleLoginError logic

* PM-2759 - SsoComp - Deprecate resetMasterPassword and replace with AccountDecryptionOptions logic + update tests

* PM-2759 - SsoComp + tests - Add trusted device encryption first draft handling which has login success and force password reset handling

* PM-2759 - Minor SsoComp comment and method name tweaks

* PM-2759 - BaseTwoFactorComp - (1) Comment out TDE stuff for now (2) Add test suite (3) Replace global window in base comp constructor with angular injection token for window which follows best practices and allows for mocking so the comp can be unit tested

* PM-2759 - Update child 2FA components to use angular injection token for window like base comp

* PM-2759 - TwoFactorComp - Finish testing all logic in doSubmit

* PM-2759 - TwoFactorComponent - Refactor DoSubmit method logic into multiple simple functions to make logic easier to follow

* PM-2759 - Add newtrustedDeviceOption.hasManageResetPasswordPermission property to match server changes

* PM-2759 - Flag AuthResult.resetMasterPassword property as deprecated

* PM-2759 - SSO comp - TDE routing logic - User without MP and ResetPassword permission must set a MP

* PM-2759 - Update Sso Comp tests to reflect additionally added TDE > MP set required logic (when user has no MP but they can reset other user passwords)

* PM-2759 - SsoComp - Add comment explaining the happy paths better for TDE success navigation

* PM-2759 - SsoComp - Refactor isTrustedDeviceEncEnabled logic into own method

* PM-2759 - SsoComp - As the 2FA comp passes the org id through to each route, going to standardize on doing so across the board for now to avoid any tricky scenarios down the line where it is needed and it's not present

* PM-2759 - SsoComp - Finish renaming orgIdFromState to orgIdentifier

* PM-2759 - SsoComp - update tests for forcePasswordReset flows now passing orgIdentifier as query param

* PM-2759 - SsoComp Tests - Export mockAcctDecryptionOpts permutations so we can share them across SsoComp and TwoFactorComp tests

* PM-2759 - Refactor 2FA comp post login redirect logic to match SSO component + add TDE logic

* PM-2759 - SsoComp - Refactor tests a bit for improved re-use

* PM-2759 - Sso Comp tests - can't export consts from a spec file or the other spec files that import them will re-execute the whole test suite as a nested test suite. TIL.

* PM-2759 - TwoFactorComp tests - All existing navigation scenarios + new TDE scenarios should now be tested.

* PM-2759 - Web - 2FA comp - Fix build error b/c of renamed base comp prop (identifier --> orgIdentifier)

* PM-2759 - Fix SsoLogin strategy tests b/c they were broken w/ the addition of the HasManageResetPasswordPermission prop to the TrustedDeviceOption interface

* PM-2759 - Web TwoFactorComp - goAfterLogIn method must be an arrow function to inherit the parent base component scope so that important things like angular services can be defined. Web 2FA flow does not work without this being an arrow func.

* PM-2759 - Fix typo

* PM-2759 - SsoComp and TwoFactorComp tests -  move service and other mocks into the top level before each to better ensure no crossover between test states per PR feedback

* PM-2759 - SsoComp - add clarity by refactoring unclear comment

* PM-2759 - SsoComp - Per excellent PR feedback, refactor if else statements to  guard statements for better readability / design

* PM-2759 - TwoFactorComp - Replace ifs with guard statements

* PM-2759 - TwoFactorComp - add clarity to comment per PR feedback

* PM-2759 - Replace use of jest.Mocked with MockProxy per PR feedback

* PM-2759 - Use unknown over any per PR feedback

* Bypass Master Password Reprompt if a user does not have a MP set (#5600)

* Add a check for a master password in PasswordRepromptService.enabled()

* Add tests for enabled()

* Update state service method call

* Use UserVerificationService to determine if a user has a master password

* rename password hash to master key hash

* fix cli build from key hash renaming

* [PM-1339] Allow Rotating Device Keys (#5806)

* Merge remote-tracking branch 'origin/feature/trusted-device-encryption' into Auth/pm-1339/rotate-device-keys

* Implement Rotation of Current Device Keys

- Detects if you are on a trusted device
- Will rotate your keys of only this device
- Allows you to still log in through SSO and decrypt your vault because the device is still trusted

* Address PR Feedback

* Move Files to Auth Ownership

* fix: getOrgKeys returning null

* [PM-3143] Trusted device encryption: Refactor reset enroll service (#5869)

* create new reset enrollment service

* refactor: login decryption options according to TODO

* feat: add tests

* PM-3143 - Add override to overriden methods

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>

* generate a master key from master password if needed (#5870)

* [PM-3120] fix: device key not being saved properly (#5882)

* pm-2582 Moved code to cipher service (#5818)

* Auth/pm 1050/pm 1051/remaining tde approval flows (#5864)

* fix: remove `Unauth guard` from `/login-with-device`

* Turned encryption on (#5908)

* [PM-3101] Fix autofill items not working for users without a master password (#5885)

* Add service factories for user verification services

* Update autofill service to check for existence of master password for autofill

* Update the context menu to check for existence of master password for autofill

* context menu test fixes

* [PM-3210] fix: use back navigation (#5907)

* Removed buttons (#5935)

* PM-2759 - Fix broken backwards compatibility for authResult.resetMast… (#5940)

* PM-2759 - Fix broken backwards compatibility for authResult.resetMasterPassword

* PM-2759 - Update TODO with specific tech debt task + target release date

* TDE - State Svc - setDeviceKey should support setting null for future support of clearing device key. (#5942)

* Check if a user has a mp before showing kdf warning (#5929)

* [PM-1200] Unlock settings changes for accounts without master password - clients (#5894)

* [PM-1200] chore: add comment for jake

* [PM-1200] chore: rename to `vault-timeout`

* [PM-1200] feat: initial version of `getAvailableVaultTimeoutActions`

* [PM-1200] feat: implement `getAvailableVaultTimeoutActions`

* [PM-1200] feat: change helper text if only logout is available

* [PM-1200] feat: only show available timeout actions

* [PM-1200] fix: add new service factories and dependencies

* [PM-1200] fix: order of dependencies

`UserVerificationService` is needed by `VaultTimeoutSettingsService`

* [PM-1200] feat: add helper text if no lock method added

* [PM-1200] refactor: simplify prev/new values when changing timeout and action

* [PM-1200] feat: fetch timeout action from new observable

* [PM-1200] refactor: make `getAvailableVaultTimeoutActions` private

* [PM-1200] feat: add test cases for `vaultTimeoutAction$`

* [PM-1200] feat: implement new timeout action logic

* [PM-1200] feat: add dynamic lock options to browser

* [PM-1200] feat: enable/disable action select

* [PM-1200] feat: add support for biometrics

* [PM-1200] feat: add helper text and disable unavailable options

* [PM-1200] feat: update action on unlock method changes

* [PM-1200] feat: update browser to use async pipe

* [PM-1200] fix: element not updating

* [PM-1200] feat: hide masterPassOnRestart pin option

* [PM-1200] feat: hide change master password from browser settins

* [PM-1200] feat: hide change master password from app menu

* [PM-1200] feat: logout if lock is not supported

* [PM-1200] feat: auto logout from lock screen if unlocking is not supported

* [PM-1200] feat: remove lock button from web menus

* Revert "[PM-1200] fix: element not updating"

This reverts commit b27f425f48570d0d5dbc9dedb9797023fef64d8b.

* Revert "[PM-1200] feat: update browser to use async pipe"

This reverts commit 766c15bc3dbadcf7dcef3053b148e7874f8939ce.

* [PM-1200] chore: add comment regarding detectorRef

* [PM-1200] feat: remove lock now button from browser settings

* [PM-1200] feat: add `userId` to unlock settings related methods

* [PM-1200] feat: remove non-lockable accounts from menu

* [PM-1200] fix: cli not building

---------

Co-authored-by: Todd Martin <tmartin@bitwarden.com>
Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>

* [PM-3215][PM-3289] Create MasterKey from Password If Needed (#5931)

* Create MasterKey from Password

- Check if the MasterKey is stored or not
- Create it if it's not

* Add getOrDeriveKey Helper

* Use Helper In More Places

* Changed settings menu to be enabled whenever the account is not locked. (#5965)

* [PM-3169] Login decryption options in extension popup (#5909)

* [PM-3169] refactor: lock guard and add new redirect guard

* [PM-3169] feat: implement fully rewritten routing

* [PM-3169] feat: close SSO window

* [PM-3169] feat: store sso org identifier in state

* [PM-3169] fix: tests

* [PM-3169] feat: get rid of unconventional patch method

* PM-3169 - SSO & 2FA Comps - Update naming of new callback to match existing pattern + add tests for callback logic execution.

* PM-3169 - Update LockGuard to have a special exception for allowing the TDE Login with MP flow

* PM-3169 - Per discussion w/ Jake and Justin, rename login-initiated guard to be tde decryption required guard (more named for functionality vs specific route)

* PM-3169 - Add some additional context to new redirect guard scenario

* PM-3169 - Per PR feedback, replace all callback types with Promise<void> as the return values are not being used.

* PM-3169 - StateSvc - Per PR feedback, update setUserSsoOrganizationIdentifier signature to explicitly use null instead of partial<string> which doesn't do anything

* PM-3169 - Replace onSuccessfulLogin type to compile

* PM-3169 - Add clarification comment for why we are not using a query param for persisting the org identifier

* PM-3169 - Per discussion with Justin, only use memory for SsoOrgId as we don't need to persist it beyond that; tested and it worked on all 3 clients for new user TDE creation

* PM-3169 - Add missing ssoIdentifierRequired translation to desktop and browser

* PM-3169 - After discussing with Justin again, we realized that memory doesn't work on desktop if user refreshes app or closes and re-opens it so must use disk.

* PM-3169 - Per PR feedback, remove hasEverHadUserKey logic as we can just leverage existing getUserKey method to check if we have a user key or not; tested all guards in browser and web with no issues

* PM-3169 - Per design discussion with Danielle, move account created toast after successful account creation vs on load of page.

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Jared Snider <jsnider@bitwarden.com>

* [PM-3314] Fixed missing MP prompt on lock component (#5966)

* Updated lock component to handle no master password.

* Added a comment.

* Add Missing Slash (#5967)

* Fix AdminAuthRequest Serialization on Desktop (#5970)

- toJSON isn't being called by ElectronStorageService
- Force it's conversion to JSON earlier so it happens for all storage methods

* Fix issue where we were incorrectly calling setRememberEmailValues in the AdminAuthRequest state - no need to do this as the email is already saved to state. By calling this method, we would actually overwrite the already saved email with null as the user's choice to remember email wasn't persisted through SSO on the login service. (#5972)

* PM-3329 - Restore everHadUserKey logic from PM-3169 which I incorrectly removed in order to fix routing logic so that user can lock and land on the lock screen properly (#5979)

* PM-3210 - TDE - LoginWithDevice routing fix - Mirror PR #5950 in just simply providing a back action on click which works for all app generated scenarios (#5982)

* PM-3332 - TDE - SsoLoginStrategy - For existing admin auth reqs, must… (#5980)

* PM-3332 - TDE - SsoLoginStrategy - For existing admin auth reqs, must manually handle 404 error case to prevent app from hanging and clear the local state if the admin auth req in the DB has been purged; i.e., it should fail silently.

* Add TODO for SSO Login Strategy tests

* PM-3331 - TDE - Firefox - Browser extension - fix access denied error… (#5984)

* PM-3331 - TDE - Firefox - Browser extension - fix access denied error on popup load which was caused by the canAccessFeature guard failing to lookup the TDE feature flag as the server config was returning null even after a successful server call as only returned the value if the user was unauthenticated for some reason

* PM-3331 - After discussion with Andre, further refactor ConfigService logic to always return the latest information from the server so that requests for feature flag data will always get the most up to date information.

* PM-3345 - TDE - Desktop - Biometrics setting submenu tweak - do not s… (#5988)

* PM-3345 - TDE - Desktop - Biometrics setting submenu tweak - do not show require MP or PIN entry on restart if user doesn't have at least one of those options b/c otherwise user can get into a bad state where they cannot unlock

* PM-3345 - TDE - Desktop - Settings comp - if user turns off PIN and Biometric is on + require PIN on restart is enabled then must turn that setting off to prevent bad user state

* PM-3345 - Final tweak to logic

* [PM-2852] Final merge from Key Migration branch to TDE Feature Branch (#5977)

* [PM-3121] Added new copy with exclamation mark

* [PM 3219] Fix key migration locking up the Desktop app (#5990)

* Only check to migrate key on VaultTimeout startup

* Remove desktop specific check

* PM-3332 - LoginWithDevice - Add error handling logic around admin auth request retrieval similar to sso login strategy to prevent error state and allow re-creation of an admin auth request if it has been purged from the server for whatever reason. (#5991)

* PM-3355 - TDE - Browser JIT Account Creation - Browser create user logic still had logic for simply closing the extension tab but as we no longer open the login decryption options in a tab we needed to update the logic here to navigate the user directly onto the vault. (#5993)

* Add distinctUntilChanged to fix multiple value changes for biometrics firing (#5999)

* Add optional chaining to master key (#6007)

* PM-3369 - TDE - Persist user's choice to trust device to state when user ma… (#6000)

* PM-3369 - Persist user's choice to trust device to state when user makes choice + persist previous choices out of state

* PM-3369 - Must set trust device in state on load if it's never been set before

* PM-3369 - Refactor BaseLoginDecOptions to properly set trust device choice in state on load

* Update libs/angular/src/auth/components/base-login-decryption-options.component.ts

Co-authored-by: Jake Fink <jfink@bitwarden.com>

---------

Co-authored-by: Jake Fink <jfink@bitwarden.com>

* Updated email change component to getOrDeriveMasterKey (#6009)

* [PM-3330] Force Update to Lockable Accounts on PIN/Biometric Update (#6006)

* Add Listener For Events that Need To Redraw the Menu

* Send redrawMenu Message When Pin/Biometrics Updated

* DeviceTrustCryptoService - don't worry about checking if a device should establish trust or not if the user doesn't have trusted device encryption on (#6010)

* Auth / pm 3351 / TDE Login - Browser & Desktop vault sync issue fix (#6002)

* PM-3351 - TDE Login on desktop and browser via SSO comp with no 2FA should trigger sync like standard onSuccessfulLogin process used to so user lands on vault with data.

* PM-3351 - 2FA Comp - Refactor onSuccessfulLogin logic to only execute in the success path just like the SSO component + adding specific onSuccessfulLoginTde flow just like SSO comp. + removed unnecessary calls to loginService.clearValues(). Added browser & desktop definitions for onSuccessfulLoginTde which is just a fullSync kick off.

* TODO

* PM-3351 - remove await to restore code back to previous state without hang.

* PM-3351 - 2FA Comp - Don't await onSuccessfulLoginTde b/c it causes a hang

* PM-3351 - remove sso comp incorrect todo

* PM-3351 - SsoComp - don't await onSuccessfulLoginTde for browsers sake

* PM-3351 - SsoComp - remove awaits from  onSuccessfulLoginTde and onSuccessfulLogin to avoid any hangs on desktop and browser

* PM-3351 - Convert onSuccessfulLoginTde to promise<void> as its return is not used + refactor all to be consistent and clearly communciate that the sync won't be awaited.

* PM-3351 - Convert onSuccessfulLogin to promise<void> and update all methods accordingly to more clearly indicate that the syncs and any other logic won't be awaited.

* [PM-3356] Fallback to OTP When MasterPassword Hasn't Been Used (#6017)

* Fallback to OTP When MasterPassword Hasn't Been Used

* Update Test and Rename Method

* Revert "DeviceTrustCryptoService - don't worry about checking if a device should establish trust or not if the user doesn't have trusted device encryption on (#6010)" (#6020)

This reverts commit 6ec22f9570.

* PM-3390 - TDE - Redraw desktop after user creation to update isLocked checks and get menu to be enabled properly (#6018)

* [PM-3383] Hide Change Password menu option for user with no MP (#6022)

* Hide Change Master Password menu item on desktop when a user doesn't have a master password.

* Renamed variable for consistency.

* Updated to base logic on account.

* Fixed menubar

* Resolve merge errors in crypto service spec

* Fixed autofill to use new method on userVerificationService (#6029)

* conflict resolution

* missing file

* PM-3456 - TDE Admin Auth Req Flow - FF dead object issue - The foreground popup must retrieve the long lived background services for the new TDE services (the AuthRequestCryptoService service fixes this issue, but the DeviceTrustCryptoService should have been added to services.module as well) (#6037)

* skip auto key check when using biometrics on browser (#6041)

* Added comments for backward compatibility removal. (#6039)

* Updated warning message. (#6059)

* Tde pr feedback (#6051)

* move pin migration to the crypto service

* refactor config service logic

* refactor lock component load logic

* rename key connector methods

* add date to backwards compat todo

* update backwards compat todo

* don't specify defaults in redirectGuard

* nit

* add null & undefined check for userid before using the account

* fix ui tests

* add todo for tech debt

* add todo comment

* Fix storybook per PR feedback

* Desktop & Browser - lock comp - add optional chaining check for focusable input - user can just have biometric and not have a MP or a PIN so must support that.

* Main.background.ts - remove duplicate instantiations of the userVerificationApiService and userVerificationService which were added in two separate PRs

* Per PR feedback - (1) Browser app routing module - fix incorrect import for redirect guard (2) Created index.ts file for auth guards to simplify imports and updated imports

* Per PR feedback, (1) Update jslib-services.module to provide actual instance of VaultTimeoutService (2) Update init service to use concrete VaultTimeoutService vs abstraction.

Co-authored-by: Matt Gibson <git@mgibson.dev>

* Per PR feedback - update services module AuthRequestCryptoService and DeviceTrustCryptoService to use shorthand format.

* Per PR feedback, add devicesService to main background and update services module to ensure the popup leverages the background devicesService

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
Co-authored-by: Matt Gibson <git@mgibson.dev>

* Updated message keys for CrowdIn to pick them up. (#6066)

* TDE PR Feedback resolutions round 2 (#6068)

* Per PR feedback - main.background.ts - move userVerificationService and userVerificationApiService to correct location

* Per PR feedback - JS lib services + vault timeout service updates - (1) Correctly type callbacks based on injection tokens (2) Update vault timeout service to have proper types based on injection tokens

* Per PR Feedback - update web init service to inject actual VaultTimeoutService vs abstraction similar to what we did for desktop here: 55a797d4ff

* Per more feedback - revert incorrect changes to VaultTimeoutService based on existing injection token types for LOGOUT_CALLBACK and LOCKED_CALLBACK.. and instead update the injection token types themselves to match how they are being used.

* Per PR feedback - in browser main.background.ts, inject concrete VaultTimeoutService instead of abstraction so we don't have to cast it anymore (matching web & desktop)

* Conflict resolution

* PM-2669 Added missing changes from conflict resolution

* Turn cipher encryption on for testing purposes

* Bumped up minimum version

* Turn off cipher key encryption

* Converted to jest-mock-extended and removed dependency

* Remove key from cipher view

* Added comment to Cipher for future refactoring (#6175)

* Remove ConfigApiServiceAbstraction from popup services (#6174)

* Replaced null orgId. (#6208)

* Added reference to new aesGenerateKey function. (#6222)

* Updated server version and feature flag for QA smoke tests.

* [PM-2814] Add ConfigService to CipherService (#6239)

* Updated CipherService to use ConfigService

Updated version check.

* Added missing DI for CLI.

* Updated parameter name for consistency.

* Addressed use of options pattern in config-service.factory.ts.

* Added CLI initialization. (#6266)

* Updated checkServerMeetsVersionRequirement to use observable (#6270)

* [PM-2814] Handle key rotation missing key (#6267)

* Fixed issue with key rotation

* Updates to CipherService to handle not having key on the model.

* More refactoring.

* Updated abstraction to remove private method.

* Fixed test.

* Updated test to reflect the fact that we set key to null.

* Resolved merge conflicte with logService added in master.

* Updated Mv3 factory include log service from merge in ConfigService initialization.

* Fixed another merge conflict with ConfigService to add logService dependency.

* Disable configService timer for cli (#6319)

The rxjs timer() function keeps the node process alive and stops it from exiting.
CLI should not run long enough to actually use the timer, so just remove it.

* [PM-3978] Handle sharing with org with cipher key encryption (#6370)

* Added explicit parameters to encrypt to handle org sharing.

* Updated add-edit to handle new parameter to encrypt

* Updated minimum server version for QA testing.

* Updated minimum version to `2023.8.0` and turned off cipher encryption for QA.

* Updated minimum server version in preparation for release.

* [PM-2669] PR review changes (#6415)

* Addressed PR feedback.

* Added comments and renamed parameters for clarity.

* Updated vault export to keep immediate invocation and reformat for clarity.

Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>

* Updated comment.

* Removed async that was left on saveCipherAttachment accidentally.

---------

Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
Co-authored-by: gbubemismith <gsmithwalter@gmail.com>
Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Jacob Fink <jfink@bitwarden.com>
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com>
Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <andreas@andreascoroiu.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: André Bispo <abispo@bitwarden.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com>
Co-authored-by: Matt Gibson <git@mgibson.dev>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Todd Martin 2023-09-28 08:44:57 -04:00 committed by GitHub
parent fe0ef5aad7
commit 8bef0883f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 729 additions and 205 deletions

View File

@ -1,6 +1,7 @@
{ {
"dev_flags": {}, "dev_flags": {},
"flags": { "flags": {
"showPasswordless": true "showPasswordless": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -6,6 +6,7 @@
} }
}, },
"flags": { "flags": {
"showPasswordless": true "showPasswordless": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -1,3 +1,5 @@
{ {
"flags": {} "flags": {
"enableCipherKeyEncryption": false
}
} }

View File

@ -464,7 +464,7 @@ export default class NotificationBackground {
private async getDecryptedCipherById(cipherId: string) { private async getDecryptedCipherById(cipherId: string) {
const cipher = await this.cipherService.get(cipherId); const cipher = await this.cipherService.get(cipherId);
if (cipher != null && cipher.type === CipherType.Login) { if (cipher != null && cipher.type === CipherType.Login) {
return await cipher.decrypt(); return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher));
} }
return null; return null;
} }

View File

@ -322,23 +322,6 @@ export default class MainBackground {
); );
this.searchService = new SearchService(this.logService, this.i18nService); this.searchService = new SearchService(this.logService, this.i18nService);
this.cipherService = new CipherService(
this.cryptoService,
this.settingsService,
this.apiService,
this.i18nService,
this.searchService,
this.stateService,
this.encryptService,
this.cipherFileUploadService
);
this.folderService = new BrowserFolderService(
this.cryptoService,
this.i18nService,
this.cipherService,
this.stateService
);
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.collectionService = new CollectionService( this.collectionService = new CollectionService(
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
@ -362,14 +345,6 @@ export default class MainBackground {
this.cryptoFunctionService, this.cryptoFunctionService,
logoutCallback logoutCallback
); );
this.vaultFilterService = new VaultFilterService(
this.stateService,
this.organizationService,
this.folderService,
this.cipherService,
this.collectionService,
this.policyService
);
this.passwordStrengthService = new PasswordStrengthService(); this.passwordStrengthService = new PasswordStrengthService();
@ -436,6 +411,36 @@ export default class MainBackground {
this.userVerificationApiService this.userVerificationApiService
); );
this.configApiService = new ConfigApiService(this.apiService, this.authService);
this.configService = new BrowserConfigService(
this.stateService,
this.configApiService,
this.authService,
this.environmentService,
this.logService,
true
);
this.cipherService = new CipherService(
this.cryptoService,
this.settingsService,
this.apiService,
this.i18nService,
this.searchService,
this.stateService,
this.encryptService,
this.cipherFileUploadService,
this.configService
);
this.folderService = new BrowserFolderService(
this.cryptoService,
this.i18nService,
this.cipherService,
this.stateService
);
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.cryptoService, this.cryptoService,
this.tokenService, this.tokenService,
@ -444,6 +449,15 @@ export default class MainBackground {
this.userVerificationService this.userVerificationService
); );
this.vaultFilterService = new VaultFilterService(
this.stateService,
this.organizationService,
this.folderService,
this.cipherService,
this.collectionService,
this.policyService
);
this.vaultTimeoutService = new VaultTimeoutService( this.vaultTimeoutService = new VaultTimeoutService(
this.cipherService, this.cipherService,
this.folderService, this.folderService,
@ -533,16 +547,6 @@ export default class MainBackground {
this.messagingService this.messagingService
); );
this.configApiService = new ConfigApiService(this.apiService, this.authService);
this.configService = new BrowserConfigService(
this.stateService,
this.configApiService,
this.authService,
this.environmentService,
this.logService,
true
);
this.browserPopoutWindowService = new BrowserPopoutWindowService(); this.browserPopoutWindowService = new BrowserPopoutWindowService();
const systemUtilsServiceReloadCallback = () => { const systemUtilsServiceReloadCallback = () => {

View File

@ -0,0 +1,32 @@
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
import {
authServiceFactory,
AuthServiceInitOptions,
} from "../../../auth/background/service-factories/auth-service.factory";
import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
type ConfigApiServiceFactoyOptions = FactoryOptions;
export type ConfigApiServiceInitOptions = ConfigApiServiceFactoyOptions &
ApiServiceInitOptions &
AuthServiceInitOptions;
export function configApiServiceFactory(
cache: { configApiService?: ConfigApiServiceAbstraction } & CachedServices,
opts: ConfigApiServiceInitOptions
): Promise<ConfigApiServiceAbstraction> {
return factory(
cache,
"configApiService",
opts,
async () =>
new ConfigApiService(
await apiServiceFactory(cache, opts),
await authServiceFactory(cache, opts)
)
);
}

View File

@ -0,0 +1,49 @@
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
import {
authServiceFactory,
AuthServiceInitOptions,
} from "../../../auth/background/service-factories/auth-service.factory";
import { configApiServiceFactory, ConfigApiServiceInitOptions } from "./config-api.service.factory";
import {
environmentServiceFactory,
EnvironmentServiceInitOptions,
} from "./environment-service.factory";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
type ConfigServiceFactoryOptions = FactoryOptions & {
configServiceOptions?: {
subscribe?: boolean;
};
};
export type ConfigServiceInitOptions = ConfigServiceFactoryOptions &
StateServiceInitOptions &
ConfigApiServiceInitOptions &
AuthServiceInitOptions &
EnvironmentServiceInitOptions &
LogServiceInitOptions;
export function configServiceFactory(
cache: { configService?: ConfigServiceAbstraction } & CachedServices,
opts: ConfigServiceInitOptions
): Promise<ConfigServiceAbstraction> {
return factory(
cache,
"configService",
opts,
async () =>
new ConfigService(
await stateServiceFactory(cache, opts),
await configApiServiceFactory(cache, opts),
await authServiceFactory(cache, opts),
await environmentServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
opts.configServiceOptions?.subscribe ?? true
)
);
}

View File

@ -18,8 +18,12 @@ import {
ApiServiceInitOptions, ApiServiceInitOptions,
} from "../../../platform/background/service-factories/api-service.factory"; } from "../../../platform/background/service-factories/api-service.factory";
import { import {
CryptoServiceInitOptions, configServiceFactory,
ConfigServiceInitOptions,
} from "../../../platform/background/service-factories/config-service.factory";
import {
cryptoServiceFactory, cryptoServiceFactory,
CryptoServiceInitOptions,
} from "../../../platform/background/service-factories/crypto-service.factory"; } from "../../../platform/background/service-factories/crypto-service.factory";
import { import {
EncryptServiceInitOptions, EncryptServiceInitOptions,
@ -49,7 +53,8 @@ export type CipherServiceInitOptions = CipherServiceFactoryOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
SearchServiceInitOptions & SearchServiceInitOptions &
StateServiceInitOptions & StateServiceInitOptions &
EncryptServiceInitOptions; EncryptServiceInitOptions &
ConfigServiceInitOptions;
export function cipherServiceFactory( export function cipherServiceFactory(
cache: { cipherService?: AbstractCipherService } & CachedServices, cache: { cipherService?: AbstractCipherService } & CachedServices,
@ -68,7 +73,8 @@ export function cipherServiceFactory(
await searchServiceFactory(cache, opts), await searchServiceFactory(cache, opts),
await stateServiceFactory(cache, opts), await stateServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts), await encryptServiceFactory(cache, opts),
await cipherFileUploadServiceFactory(cache, opts) await cipherFileUploadServiceFactory(cache, opts),
await configServiceFactory(cache, opts)
) )
); );
} }

View File

@ -1,3 +1,5 @@
{ {
"flags": {} "flags": {
"enableCipherKeyEncryption": false
}
} }

View File

@ -1,3 +1,5 @@
{ {
"flags": {} "flags": {
"enableCipherKeyEncryption": false
}
} }

View File

@ -45,11 +45,15 @@ export class ShareCommand {
if (cipher.organizationId != null) { if (cipher.organizationId != null) {
return Response.badRequest("This item already belongs to an organization."); return Response.badRequest("This item already belongs to an organization.");
} }
const cipherView = await cipher.decrypt(); const cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
try { try {
await this.cipherService.shareWithServer(cipherView, organizationId, req); await this.cipherService.shareWithServer(cipherView, organizationId, req);
const updatedCipher = await this.cipherService.get(cipher.id); const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt(); const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher)
);
const res = new CipherResponse(decCipher); const res = new CipherResponse(decCipher);
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {

View File

@ -25,11 +25,13 @@ import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.ser
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { ClientType, KeySuffixOptions, LogLevelType } from "@bitwarden/common/enums"; import { ClientType, KeySuffixOptions, LogLevelType } from "@bitwarden/common/enums";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Account } from "@bitwarden/common/platform/models/domain/account"; import { Account } from "@bitwarden/common/platform/models/domain/account";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service";
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
@ -75,6 +77,7 @@ import {
} from "@bitwarden/importer"; } from "@bitwarden/importer";
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
import { CliConfigService } from "./platform/services/cli-config.service";
import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service"; import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service";
import { ConsoleLogService } from "./platform/services/console-log.service"; import { ConsoleLogService } from "./platform/services/console-log.service";
import { I18nService } from "./platform/services/i18n.service"; import { I18nService } from "./platform/services/i18n.service";
@ -147,6 +150,8 @@ export class Main {
devicesApiService: DevicesApiServiceAbstraction; devicesApiService: DevicesApiServiceAbstraction;
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
authRequestCryptoService: AuthRequestCryptoServiceAbstraction; authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
configApiService: ConfigApiServiceAbstraction;
configService: CliConfigService;
constructor() { constructor() {
let p = null; let p = null;
@ -252,28 +257,8 @@ export class Main {
this.searchService = new SearchService(this.logService, this.i18nService); this.searchService = new SearchService(this.logService, this.i18nService);
this.cipherService = new CipherService(
this.cryptoService,
this.settingsService,
this.apiService,
this.i18nService,
this.searchService,
this.stateService,
this.encryptService,
this.cipherFileUploadService
);
this.broadcasterService = new BroadcasterService(); this.broadcasterService = new BroadcasterService();
this.folderService = new FolderService(
this.cryptoService,
this.i18nService,
this.cipherService,
this.stateService
);
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.collectionService = new CollectionService( this.collectionService = new CollectionService(
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
@ -349,6 +334,38 @@ export class Main {
this.authRequestCryptoService this.authRequestCryptoService
); );
this.configApiService = new ConfigApiService(this.apiService, this.authService);
this.configService = new CliConfigService(
this.stateService,
this.configApiService,
this.authService,
this.environmentService,
this.logService,
true
);
this.cipherService = new CipherService(
this.cryptoService,
this.settingsService,
this.apiService,
this.i18nService,
this.searchService,
this.stateService,
this.encryptService,
this.cipherFileUploadService,
this.configService
);
this.folderService = new FolderService(
this.cryptoService,
this.i18nService,
this.cipherService,
this.stateService
);
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
const lockedCallback = async (userId?: string) => const lockedCallback = async (userId?: string) =>
await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto); await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto);
@ -472,6 +489,7 @@ export class Main {
const locale = await this.stateService.getLocale(); const locale = await this.stateService.getLocale();
await this.i18nService.init(locale); await this.i18nService.init(locale);
this.twoFactorService.init(); this.twoFactorService.init();
this.configService.init();
const installedVersion = await this.stateService.getInstalledVersion(); const installedVersion = await this.stateService.getInstalledVersion();
const currentVersion = await this.platformUtilsService.getApplicationVersion(); const currentVersion = await this.platformUtilsService.getApplicationVersion();

View File

@ -77,7 +77,9 @@ export class EditCommand {
return Response.notFound(); return Response.notFound();
} }
let cipherView = await cipher.decrypt(); let cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
if (cipherView.isDeleted) { if (cipherView.isDeleted) {
return Response.badRequest("You may not edit a deleted item. Use the restore command first."); return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
} }
@ -86,7 +88,9 @@ export class EditCommand {
try { try {
await this.cipherService.updateWithServer(encCipher); await this.cipherService.updateWithServer(encCipher);
const updatedCipher = await this.cipherService.get(cipher.id); const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt(); const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher)
);
const res = new CipherResponse(decCipher); const res = new CipherResponse(decCipher);
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {
@ -109,7 +113,9 @@ export class EditCommand {
try { try {
await this.cipherService.saveCollectionsWithServer(cipher); await this.cipherService.saveCollectionsWithServer(cipher);
const updatedCipher = await this.cipherService.get(cipher.id); const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt(); const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher)
);
const res = new CipherResponse(decCipher); const res = new CipherResponse(decCipher);
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {

View File

@ -103,7 +103,9 @@ export class GetCommand extends DownloadCommand {
if (Utils.isGuid(id)) { if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id); const cipher = await this.cipherService.get(id);
if (cipher != null) { if (cipher != null) {
decCipher = await cipher.decrypt(); decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
} }
} else if (id.trim() !== "") { } else if (id.trim() !== "") {
let ciphers = await this.cipherService.getAllDecrypted(); let ciphers = await this.cipherService.getAllDecrypted();

View File

@ -0,0 +1,9 @@
import { NEVER } from "rxjs";
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
export class CliConfigService extends ConfigService {
// The rxjs timer uses setTimeout/setInterval under the hood, which prevents the node process from exiting
// when the command is finished. Cli should never be alive long enough to use the timer, so we disable it.
protected refreshTimer$ = NEVER;
}

View File

@ -80,7 +80,9 @@ export class CreateCommand {
try { try {
await this.cipherService.createWithServer(cipher); await this.cipherService.createWithServer(cipher);
const newCipher = await this.cipherService.get(cipher.id); const newCipher = await this.cipherService.get(cipher.id);
const decCipher = await newCipher.decrypt(); const decCipher = await newCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(newCipher)
);
const res = new CipherResponse(decCipher); const res = new CipherResponse(decCipher);
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {
@ -141,7 +143,9 @@ export class CreateCommand {
new Uint8Array(fileBuf).buffer new Uint8Array(fileBuf).buffer
); );
const updatedCipher = await this.cipherService.get(cipher.id); const updatedCipher = await this.cipherService.get(cipher.id);
const decCipher = await updatedCipher.decrypt(); const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher)
);
return Response.success(new CipherResponse(decCipher)); return Response.success(new CipherResponse(decCipher));
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);

View File

@ -1,6 +1,7 @@
{ {
"dev_flags": {}, "dev_flags": {},
"flags": { "flags": {
"multithreadDecryption": false "multithreadDecryption": false,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -1,6 +1,7 @@
{ {
"devFlags": {}, "devFlags": {},
"flags": { "flags": {
"showDDGSetting": true "showDDGSetting": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -1,5 +1,6 @@
{ {
"flags": { "flags": {
"showDDGSetting": true "showDDGSetting": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -1,4 +1,4 @@
{ {
"bitwarden": { "bitwarden": {
"message": "Bitwarden" "message": "Bitwarden"
}, },

View File

@ -190,7 +190,9 @@ export class EncryptedMessageHandlerService {
if (cipher === null) { if (cipher === null) {
return { status: "failure" }; return { status: "failure" };
} }
const cipherView = await cipher.decrypt(); const cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
cipherView.name = credentialUpdatePayload.name; cipherView.name = credentialUpdatePayload.name;
cipherView.login.password = credentialUpdatePayload.password; cipherView.login.password = credentialUpdatePayload.password;
cipherView.login.username = credentialUpdatePayload.userName; cipherView.login.username = credentialUpdatePayload.userName;

View File

@ -12,6 +12,7 @@
}, },
"flags": { "flags": {
"secretsManager": false, "secretsManager": false,
"showPasswordless": false "showPasswordless": false,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -18,6 +18,7 @@
}, },
"flags": { "flags": {
"secretsManager": true, "secretsManager": true,
"showPasswordless": true "showPasswordless": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -11,6 +11,7 @@
}, },
"flags": { "flags": {
"secretsManager": true, "secretsManager": true,
"showPasswordless": true "showPasswordless": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -12,6 +12,7 @@
}, },
"flags": { "flags": {
"secretsManager": true, "secretsManager": true,
"showPasswordless": true "showPasswordless": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -12,6 +12,7 @@
}, },
"flags": { "flags": {
"secretsManager": true, "secretsManager": true,
"showPasswordless": true "showPasswordless": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -8,6 +8,7 @@
}, },
"flags": { "flags": {
"secretsManager": false, "secretsManager": false,
"showPasswordless": true "showPasswordless": true,
"enableCipherKeyEncryption": false
} }
} }

View File

@ -110,7 +110,7 @@ export class AddEditComponent extends BaseAddEditComponent {
if (!this.organization.canEditAnyCollection) { if (!this.organization.canEditAnyCollection) {
return super.encryptCipher(); return super.encryptCipher();
} }
return this.cipherService.encrypt(this.cipher, null, this.originalCipher); return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher);
} }
protected async deleteCipher() { protected async deleteCipher() {

View File

@ -37,7 +37,9 @@ export class CollectionsComponent implements OnInit {
async load() { async load() {
this.cipherDomain = await this.loadCipher(); this.cipherDomain = await this.loadCipher();
this.collectionIds = this.loadCipherCollections(); this.collectionIds = this.loadCipherCollections();
this.cipher = await this.cipherDomain.decrypt(); this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain)
);
this.collections = await this.loadCollections(); this.collections = await this.loadCollections();
this.collections.forEach((c) => ((c as any).checked = false)); this.collections.forEach((c) => ((c as any).checked = false));

View File

@ -66,7 +66,9 @@ export class ShareComponent implements OnInit, OnDestroy {
}); });
const cipherDomain = await this.cipherService.get(this.cipherId); const cipherDomain = await this.cipherService.get(this.cipherId);
this.cipher = await cipherDomain.decrypt(); this.cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain)
);
this.filterCollections(); this.filterCollections();
} }
@ -94,7 +96,9 @@ export class ShareComponent implements OnInit, OnDestroy {
} }
const cipherDomain = await this.cipherService.get(this.cipherId); const cipherDomain = await this.cipherService.get(this.cipherId);
const cipherView = await cipherDomain.decrypt(); const cipherView = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain)
);
const orgs = await firstValueFrom(this.organizations$); const orgs = await firstValueFrom(this.organizations$);
const orgName = const orgName =
orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization"); orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization");

View File

@ -271,7 +271,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
searchService: SearchServiceAbstraction, searchService: SearchServiceAbstraction,
stateService: StateServiceAbstraction, stateService: StateServiceAbstraction,
encryptService: EncryptService, encryptService: EncryptService,
fileUploadService: CipherFileUploadServiceAbstraction fileUploadService: CipherFileUploadServiceAbstraction,
configService: ConfigServiceAbstraction
) => ) =>
new CipherService( new CipherService(
cryptoService, cryptoService,
@ -281,7 +282,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
searchService, searchService,
stateService, stateService,
encryptService, encryptService,
fileUploadService fileUploadService,
configService
), ),
deps: [ deps: [
CryptoServiceAbstraction, CryptoServiceAbstraction,
@ -292,6 +294,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
StateServiceAbstraction, StateServiceAbstraction,
EncryptService, EncryptService,
CipherFileUploadServiceAbstraction, CipherFileUploadServiceAbstraction,
ConfigServiceAbstraction,
], ],
}, },
{ {

View File

@ -225,7 +225,9 @@ export class AddEditComponent implements OnInit, OnDestroy {
if (this.cipher == null) { if (this.cipher == null) {
if (this.editMode) { if (this.editMode) {
const cipher = await this.loadCipher(); const cipher = await this.loadCipher();
this.cipher = await cipher.decrypt(); this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
// Adjust Cipher Name if Cloning // Adjust Cipher Name if Cloning
if (this.cloneMode) { if (this.cloneMode) {

View File

@ -73,7 +73,9 @@ export class AttachmentsComponent implements OnInit {
try { try {
this.formPromise = this.saveCipherAttachment(files[0]); this.formPromise = this.saveCipherAttachment(files[0]);
this.cipherDomain = await this.formPromise; this.cipherDomain = await this.formPromise;
this.cipher = await this.cipherDomain.decrypt(); this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain)
);
this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved")); this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved"));
this.onUploadedAttachment.emit(); this.onUploadedAttachment.emit();
} catch (e) { } catch (e) {
@ -179,7 +181,9 @@ export class AttachmentsComponent implements OnInit {
protected async init() { protected async init() {
this.cipherDomain = await this.loadCipher(); this.cipherDomain = await this.loadCipher();
this.cipher = await this.cipherDomain.decrypt(); this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain)
);
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await this.stateService.getCanAccessPremium();
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;
@ -229,7 +233,9 @@ export class AttachmentsComponent implements OnInit {
decBuf, decBuf,
admin admin
); );
this.cipher = await this.cipherDomain.decrypt(); this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain)
);
// 3. Delete old // 3. Delete old
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);

View File

@ -33,7 +33,9 @@ export class PasswordHistoryComponent implements OnInit {
protected async init() { protected async init() {
const cipher = await this.cipherService.get(this.cipherId); const cipher = await this.cipherService.get(this.cipherId);
const decCipher = await cipher.decrypt(); const decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
} }
} }

View File

@ -113,7 +113,9 @@ export class ViewComponent implements OnDestroy, OnInit {
this.cleanUp(); this.cleanUp();
const cipher = await this.cipherService.get(this.cipherId); const cipher = await this.cipherService.get(this.cipherId);
this.cipher = await cipher.decrypt(); this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
this.canAccessPremium = await this.stateService.getCanAccessPremium(); this.canAccessPremium = await this.stateService.getCanAccessPremium();
this.showPremiumRequiredTotp = this.showPremiumRequiredTotp =
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;

View File

@ -88,6 +88,7 @@ export class CipherExport {
domain.notes = req.notes != null ? new EncString(req.notes) : null; domain.notes = req.notes != null ? new EncString(req.notes) : null;
domain.favorite = req.favorite; domain.favorite = req.favorite;
domain.reprompt = req.reprompt ?? CipherRepromptType.None; domain.reprompt = req.reprompt ?? CipherRepromptType.None;
domain.key = req.key != null ? new EncString(req.key) : null;
if (req.fields != null) { if (req.fields != null) {
domain.fields = req.fields.map((f) => FieldExport.toDomain(f)); domain.fields = req.fields.map((f) => FieldExport.toDomain(f));
@ -135,6 +136,7 @@ export class CipherExport {
revisionDate: Date = null; revisionDate: Date = null;
creationDate: Date = null; creationDate: Date = null;
deletedDate: Date = null; deletedDate: Date = null;
key: string;
// Use build method instead of ctor so that we can control order of JSON stringify for pretty print // Use build method instead of ctor so that we can control order of JSON stringify for pretty print
build(o: CipherView | CipherDomain) { build(o: CipherView | CipherDomain) {
@ -149,6 +151,7 @@ export class CipherExport {
} else { } else {
this.name = o.name?.encryptedString; this.name = o.name?.encryptedString;
this.notes = o.notes?.encryptedString; this.notes = o.notes?.encryptedString;
this.key = o.key?.encryptedString;
} }
this.favorite = o.favorite; this.favorite = o.favorite;

View File

@ -1,4 +1,5 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { SemVer } from "semver";
import { FeatureFlag } from "../../../enums/feature-flag.enum"; import { FeatureFlag } from "../../../enums/feature-flag.enum";
import { Region } from "../environment.service"; import { Region } from "../environment.service";
@ -16,6 +17,9 @@ export abstract class ConfigServiceAbstraction {
key: FeatureFlag, key: FeatureFlag,
defaultValue?: T defaultValue?: T
) => Promise<T>; ) => Promise<T>;
checkServerMeetsVersionRequirement$: (
minimumRequiredServerVersion: SemVer
) => Observable<boolean>;
/** /**
* Force ConfigService to fetch an updated config from the server and emit it from serverConfig$ * Force ConfigService to fetch an updated config from the server and emit it from serverConfig$

View File

@ -6,6 +6,7 @@ import { KeySuffixOptions, KdfType, HashPurpose } from "../../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { import {
CipherKey,
MasterKey, MasterKey,
OrgKey, OrgKey,
PinKey, PinKey,
@ -372,6 +373,11 @@ export abstract class CryptoService {
*/ */
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>; rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
randomNumber: (min: number, max: number) => Promise<number>; randomNumber: (min: number, max: number) => Promise<number>;
/**
* Generates a new cipher key
* @returns A new cipher key
*/
makeCipherKey: () => Promise<CipherKey>;
/** /**
* Initialize all necessary crypto keys needed for a new account. * Initialize all necessary crypto keys needed for a new account.

View File

@ -8,5 +8,5 @@ import { InitializerMetadata } from "./initializer-metadata.interface";
* @example Cipher implements Decryptable<CipherView> * @example Cipher implements Decryptable<CipherView>
*/ */
export interface Decryptable<TDecrypted extends InitializerMetadata> extends InitializerMetadata { export interface Decryptable<TDecrypted extends InitializerMetadata> extends InitializerMetadata {
decrypt: (key?: SymmetricCryptoKey) => Promise<TDecrypted>; decrypt: (key: SymmetricCryptoKey) => Promise<TDecrypted>;
} }

View File

@ -3,6 +3,7 @@
export type SharedFlags = { export type SharedFlags = {
multithreadDecryption: boolean; multithreadDecryption: boolean;
showPasswordless?: boolean; showPasswordless?: boolean;
enableCipherKeyEncryption?: boolean;
}; };
// required to avoid linting errors when there are no flags // required to avoid linting errors when there are no flags

View File

@ -83,3 +83,4 @@ export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">; export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">; export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">;
export type ProviderKey = Opaque<SymmetricCryptoKey, "ProviderKey">; export type ProviderKey = Opaque<SymmetricCryptoKey, "ProviderKey">;
export type CipherKey = Opaque<SymmetricCryptoKey, "CipherKey">;

View File

@ -10,6 +10,7 @@ import {
merge, merge,
timer, timer,
} from "rxjs"; } from "rxjs";
import { SemVer } from "semver";
import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@ -25,10 +26,13 @@ import { ServerConfigData } from "../../models/data/server-config.data";
const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600; const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600;
export class ConfigService implements ConfigServiceAbstraction { export class ConfigService implements ConfigServiceAbstraction {
private inited = false;
protected _serverConfig = new ReplaySubject<ServerConfig | null>(1); protected _serverConfig = new ReplaySubject<ServerConfig | null>(1);
serverConfig$ = this._serverConfig.asObservable(); serverConfig$ = this._serverConfig.asObservable();
private _forceFetchConfig = new Subject<void>(); private _forceFetchConfig = new Subject<void>();
private inited = false; protected refreshTimer$ = timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS); // after 1 hour, then every hour
cloudRegion$ = this.serverConfig$.pipe( cloudRegion$ = this.serverConfig$.pipe(
map((config) => config?.environment?.cloudRegion ?? Region.US) map((config) => config?.environment?.cloudRegion ?? Region.US)
@ -62,7 +66,7 @@ export class ConfigService implements ConfigServiceAbstraction {
// If you need to fetch a new config when an event occurs, add an observable that emits on that event here // If you need to fetch a new config when an event occurs, add an observable that emits on that event here
merge( merge(
timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS), // after 1 hour, then every hour this.refreshTimer$, // an overridable interval
this.environmentService.urls, // when environment URLs change (including when app is started) this.environmentService.urls, // when environment URLs change (including when app is started)
this._forceFetchConfig // manual this._forceFetchConfig // manual
) )
@ -103,4 +107,21 @@ export class ConfigService implements ConfigServiceAbstraction {
await this.stateService.setServerConfig(data); await this.stateService.setServerConfig(data);
this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion); this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion);
} }
/**
* Verifies whether the server version meets the minimum required version
* @param minimumRequiredServerVersion The minimum version required
* @returns True if the server version is greater than or equal to the minimum required version
*/
checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) {
return this.serverConfig$.pipe(
map((serverConfig) => {
if (serverConfig == null) {
return false;
}
const serverVersion = new SemVer(serverConfig.version);
return serverVersion.compare(minimumRequiredServerVersion) >= 0;
})
);
}
} }

View File

@ -27,6 +27,7 @@ import { EFFLongWordList } from "../misc/wordlist";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { import {
CipherKey,
MasterKey, MasterKey,
OrgKey, OrgKey,
PinKey, PinKey,
@ -596,6 +597,11 @@ export class CryptoService implements CryptoServiceAbstraction {
return new SymmetricCryptoKey(sendKey); return new SymmetricCryptoKey(sendKey);
} }
async makeCipherKey(): Promise<CipherKey> {
const randomBytes = await this.cryptoFunctionService.aesGenerateKey(512);
return new SymmetricCryptoKey(randomBytes) as CipherKey;
}
async clearKeys(userId?: string): Promise<any> { async clearKeys(userId?: string): Promise<any> {
await this.clearUserKey(true, userId); await this.clearUserKey(true, userId);
await this.clearMasterKeyHash(userId); await this.clearMasterKeyHash(userId);

View File

@ -11,7 +11,8 @@ export abstract class CipherService {
clearCache: (userId?: string) => Promise<void>; clearCache: (userId?: string) => Promise<void>;
encrypt: ( encrypt: (
model: CipherView, model: CipherView,
key?: SymmetricCryptoKey, keyForEncryption?: SymmetricCryptoKey,
keyForCipherKeyDecryption?: SymmetricCryptoKey,
originalCipher?: Cipher originalCipher?: Cipher
) => Promise<Cipher>; ) => Promise<Cipher>;
encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise<Field[]>; encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise<Field[]>;
@ -81,4 +82,5 @@ export abstract class CipherService {
organizationId?: string, organizationId?: string,
asAdmin?: boolean asAdmin?: boolean
) => Promise<void>; ) => Promise<void>;
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>;
} }

View File

@ -33,6 +33,7 @@ export class CipherData {
creationDate: string; creationDate: string;
deletedDate: string; deletedDate: string;
reprompt: CipherRepromptType; reprompt: CipherRepromptType;
key: string;
constructor(response?: CipherResponse, collectionIds?: string[]) { constructor(response?: CipherResponse, collectionIds?: string[]) {
if (response == null) { if (response == null) {
@ -54,6 +55,7 @@ export class CipherData {
this.creationDate = response.creationDate; this.creationDate = response.creationDate;
this.deletedDate = response.deletedDate; this.deletedDate = response.deletedDate;
this.reprompt = response.reprompt; this.reprompt = response.reprompt;
this.key = response.key;
switch (this.type) { switch (this.type) {
case CipherType.Login: case CipherType.Login:

View File

@ -2,10 +2,14 @@
import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { mockEnc, mockFromJson } from "../../../../spec"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
import { FieldType, SecureNoteType, UriMatchType } from "../../../enums"; import { FieldType, SecureNoteType, UriMatchType } from "../../../enums";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string"; import { EncString } from "../../../platform/models/domain/enc-string";
import { ContainerService } from "../../../platform/services/container.service";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { CipherService } from "../../abstractions/cipher.service";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type"; import { CipherType } from "../../enums/cipher-type";
import { CipherData } from "../../models/data/cipher.data"; import { CipherData } from "../../models/data/cipher.data";
@ -47,6 +51,7 @@ describe("Cipher DTO", () => {
attachments: null, attachments: null,
fields: null, fields: null,
passwordHistory: null, passwordHistory: null,
key: null,
}); });
}); });
@ -69,6 +74,7 @@ describe("Cipher DTO", () => {
creationDate: "2022-01-01T12:00:00.000Z", creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null, deletedDate: null,
reprompt: CipherRepromptType.None, reprompt: CipherRepromptType.None,
key: "EncryptedString",
login: { login: {
uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }], uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }],
username: "EncryptedString", username: "EncryptedString",
@ -136,6 +142,7 @@ describe("Cipher DTO", () => {
creationDate: new Date("2022-01-01T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"),
deletedDate: null, deletedDate: null,
reprompt: 0, reprompt: 0,
key: { encryptedString: "EncryptedString", encryptionType: 0 },
login: { login: {
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"), passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
autofillOnPageLoad: false, autofillOnPageLoad: false,
@ -206,6 +213,7 @@ describe("Cipher DTO", () => {
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z"); cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
cipher.deletedDate = null; cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None; cipher.reprompt = CipherRepromptType.None;
cipher.key = mockEnc("EncKey");
const loginView = new LoginView(); const loginView = new LoginView();
loginView.username = "username"; loginView.username = "username";
@ -215,7 +223,20 @@ describe("Cipher DTO", () => {
login.decrypt(Arg.any(), Arg.any()).resolves(loginView); login.decrypt(Arg.any(), Arg.any()).resolves(loginView);
cipher.login = login; cipher.login = login;
const cipherView = await cipher.decrypt(); const cryptoService = Substitute.for<CryptoService>();
const encryptService = Substitute.for<EncryptService>();
const cipherService = Substitute.for<CipherService>();
encryptService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(64));
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService
);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher)
);
expect(cipherView).toMatchObject({ expect(cipherView).toMatchObject({
id: "id", id: "id",
@ -261,6 +282,7 @@ describe("Cipher DTO", () => {
creationDate: "2022-01-01T12:00:00.000Z", creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null, deletedDate: null,
reprompt: CipherRepromptType.None, reprompt: CipherRepromptType.None,
key: "EncKey",
secureNote: { secureNote: {
type: SecureNoteType.Generic, type: SecureNoteType.Generic,
}, },
@ -292,6 +314,7 @@ describe("Cipher DTO", () => {
attachments: null, attachments: null,
fields: null, fields: null,
passwordHistory: null, passwordHistory: null,
key: { encryptedString: "EncKey", encryptionType: 0 },
}); });
}); });
@ -318,8 +341,22 @@ describe("Cipher DTO", () => {
cipher.reprompt = CipherRepromptType.None; cipher.reprompt = CipherRepromptType.None;
cipher.secureNote = new SecureNote(); cipher.secureNote = new SecureNote();
cipher.secureNote.type = SecureNoteType.Generic; cipher.secureNote.type = SecureNoteType.Generic;
cipher.key = mockEnc("EncKey");
const cipherView = await cipher.decrypt(); const cryptoService = Substitute.for<CryptoService>();
const encryptService = Substitute.for<EncryptService>();
const cipherService = Substitute.for<CipherService>();
encryptService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(64));
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService
);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher)
);
expect(cipherView).toMatchObject({ expect(cipherView).toMatchObject({
id: "id", id: "id",
@ -373,6 +410,7 @@ describe("Cipher DTO", () => {
expYear: "EncryptedString", expYear: "EncryptedString",
code: "EncryptedString", code: "EncryptedString",
}, },
key: "EncKey",
}; };
}); });
@ -408,6 +446,7 @@ describe("Cipher DTO", () => {
attachments: null, attachments: null,
fields: null, fields: null,
passwordHistory: null, passwordHistory: null,
key: { encryptedString: "EncKey", encryptionType: 0 },
}); });
}); });
@ -432,6 +471,7 @@ describe("Cipher DTO", () => {
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z"); cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
cipher.deletedDate = null; cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None; cipher.reprompt = CipherRepromptType.None;
cipher.key = mockEnc("EncKey");
const cardView = new CardView(); const cardView = new CardView();
cardView.cardholderName = "cardholderName"; cardView.cardholderName = "cardholderName";
@ -441,7 +481,20 @@ describe("Cipher DTO", () => {
card.decrypt(Arg.any(), Arg.any()).resolves(cardView); card.decrypt(Arg.any(), Arg.any()).resolves(cardView);
cipher.card = card; cipher.card = card;
const cipherView = await cipher.decrypt(); const cryptoService = Substitute.for<CryptoService>();
const encryptService = Substitute.for<EncryptService>();
const cipherService = Substitute.for<CipherService>();
encryptService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(64));
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService
);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher)
);
expect(cipherView).toMatchObject({ expect(cipherView).toMatchObject({
id: "id", id: "id",
@ -487,6 +540,7 @@ describe("Cipher DTO", () => {
creationDate: "2022-01-01T12:00:00.000Z", creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null, deletedDate: null,
reprompt: CipherRepromptType.None, reprompt: CipherRepromptType.None,
key: "EncKey",
identity: { identity: {
title: "EncryptedString", title: "EncryptedString",
firstName: "EncryptedString", firstName: "EncryptedString",
@ -554,6 +608,7 @@ describe("Cipher DTO", () => {
attachments: null, attachments: null,
fields: null, fields: null,
passwordHistory: null, passwordHistory: null,
key: { encryptedString: "EncKey", encryptionType: 0 },
}); });
}); });
@ -578,6 +633,7 @@ describe("Cipher DTO", () => {
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z"); cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
cipher.deletedDate = null; cipher.deletedDate = null;
cipher.reprompt = CipherRepromptType.None; cipher.reprompt = CipherRepromptType.None;
cipher.key = mockEnc("EncKey");
const identityView = new IdentityView(); const identityView = new IdentityView();
identityView.firstName = "firstName"; identityView.firstName = "firstName";
@ -587,7 +643,20 @@ describe("Cipher DTO", () => {
identity.decrypt(Arg.any(), Arg.any()).resolves(identityView); identity.decrypt(Arg.any(), Arg.any()).resolves(identityView);
cipher.identity = identity; cipher.identity = identity;
const cipherView = await cipher.decrypt(); const cryptoService = Substitute.for<CryptoService>();
const encryptService = Substitute.for<EncryptService>();
const cipherService = Substitute.for<CipherService>();
encryptService.decryptToBytes(Arg.any(), Arg.any()).resolves(makeStaticByteArray(64));
(window as any).bitwardenContainerService = new ContainerService(
cryptoService,
encryptService
);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher)
);
expect(cipherView).toMatchObject({ expect(cipherView).toMatchObject({
id: "id", id: "id",

View File

@ -1,6 +1,7 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { Decryptable } from "../../../platform/interfaces/decryptable.interface"; import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
import { Utils } from "../../../platform/misc/utils";
import Domain from "../../../platform/models/domain/domain-base"; import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string"; import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@ -45,6 +46,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
creationDate: Date; creationDate: Date;
deletedDate: Date; deletedDate: Date;
reprompt: CipherRepromptType; reprompt: CipherRepromptType;
key: EncString;
constructor(obj?: CipherData, localData: LocalData = null) { constructor(obj?: CipherData, localData: LocalData = null) {
super(); super();
@ -61,6 +63,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
folderId: null, folderId: null,
name: null, name: null,
notes: null, notes: null,
key: null,
}, },
["id", "organizationId", "folderId"] ["id", "organizationId", "folderId"]
); );
@ -117,9 +120,17 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
} }
} }
async decrypt(encKey?: SymmetricCryptoKey): Promise<CipherView> { // We are passing the organizationId into the EncString.decrypt() method here, but because the encKey will always be
// present and so the organizationId will not be used.
// We will refactor the EncString.decrypt() in https://bitwarden.atlassian.net/browse/PM-3762 to remove the dependency on the organizationId.
async decrypt(encKey: SymmetricCryptoKey): Promise<CipherView> {
const model = new CipherView(this); const model = new CipherView(this);
if (this.key != null) {
const encryptService = Utils.getContainerService().getEncryptService();
encKey = new SymmetricCryptoKey(await encryptService.decryptToBytes(this.key, encKey));
}
await this.decryptObj( await this.decryptObj(
model, model,
{ {
@ -147,14 +158,12 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
break; break;
} }
const orgId = this.organizationId;
if (this.attachments != null && this.attachments.length > 0) { if (this.attachments != null && this.attachments.length > 0) {
const attachments: any[] = []; const attachments: any[] = [];
await this.attachments.reduce((promise, attachment) => { await this.attachments.reduce((promise, attachment) => {
return promise return promise
.then(() => { .then(() => {
return attachment.decrypt(orgId, encKey); return attachment.decrypt(this.organizationId, encKey);
}) })
.then((decAttachment) => { .then((decAttachment) => {
attachments.push(decAttachment); attachments.push(decAttachment);
@ -168,7 +177,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
await this.fields.reduce((promise, field) => { await this.fields.reduce((promise, field) => {
return promise return promise
.then(() => { .then(() => {
return field.decrypt(orgId, encKey); return field.decrypt(this.organizationId, encKey);
}) })
.then((decField) => { .then((decField) => {
fields.push(decField); fields.push(decField);
@ -182,7 +191,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
await this.passwordHistory.reduce((promise, ph) => { await this.passwordHistory.reduce((promise, ph) => {
return promise return promise
.then(() => { .then(() => {
return ph.decrypt(orgId, encKey); return ph.decrypt(this.organizationId, encKey);
}) })
.then((decPh) => { .then((decPh) => {
passwordHistory.push(decPh); passwordHistory.push(decPh);
@ -209,6 +218,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null; c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null;
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null; c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null;
c.reprompt = this.reprompt; c.reprompt = this.reprompt;
c.key = this.key?.encryptedString;
this.buildDataModel(this, c, { this.buildDataModel(this, c, {
name: null, name: null,
@ -257,6 +267,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a)); const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a));
const fields = obj.fields?.map((f: any) => Field.fromJSON(f)); const fields = obj.fields?.map((f: any) => Field.fromJSON(f));
const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph)); const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph));
const key = EncString.fromJSON(obj.key);
Object.assign(domain, obj, { Object.assign(domain, obj, {
name, name,
@ -266,6 +277,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
attachments, attachments,
fields, fields,
passwordHistory, passwordHistory,
key,
}); });
switch (obj.type) { switch (obj.type) {

View File

@ -29,6 +29,7 @@ export class CipherRequest {
attachments2: { [id: string]: AttachmentRequest }; attachments2: { [id: string]: AttachmentRequest };
lastKnownRevisionDate: Date; lastKnownRevisionDate: Date;
reprompt: CipherRepromptType; reprompt: CipherRepromptType;
key: string;
constructor(cipher: Cipher) { constructor(cipher: Cipher) {
this.type = cipher.type; this.type = cipher.type;
@ -39,6 +40,7 @@ export class CipherRequest {
this.favorite = cipher.favorite; this.favorite = cipher.favorite;
this.lastKnownRevisionDate = cipher.revisionDate; this.lastKnownRevisionDate = cipher.revisionDate;
this.reprompt = cipher.reprompt; this.reprompt = cipher.reprompt;
this.key = cipher.key?.encryptedString;
switch (this.type) { switch (this.type) {
case CipherType.Login: case CipherType.Login:

View File

@ -32,6 +32,7 @@ export class CipherResponse extends BaseResponse {
creationDate: string; creationDate: string;
deletedDate: string; deletedDate: string;
reprompt: CipherRepromptType; reprompt: CipherRepromptType;
key: string;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
@ -90,5 +91,6 @@ export class CipherResponse extends BaseResponse {
} }
this.reprompt = this.getResponseProperty("Reprompt") || CipherRepromptType.None; this.reprompt = this.getResponseProperty("Reprompt") || CipherRepromptType.None;
this.key = this.getResponseProperty("Key") || null;
} }
} }

View File

@ -1,15 +1,24 @@
// eslint-disable-next-line no-restricted-imports
import { mock, mockReset } from "jest-mock-extended"; import { mock, mockReset } from "jest-mock-extended";
import { of } from "rxjs";
import { makeStaticByteArray } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service"; import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service"; import { SettingsService } from "../../abstractions/settings.service";
import { UriMatchType, FieldType } from "../../enums"; import { UriMatchType, FieldType } from "../../enums";
import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service"; import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { OrgKey, SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string";
import {
CipherKey,
OrgKey,
SymmetricCryptoKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../platform/services/container.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { CipherRepromptType } from "../enums/cipher-reprompt-type"; import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type"; import { CipherType } from "../enums/cipher-type";
@ -18,9 +27,13 @@ import { Cipher } from "../models/domain/cipher";
import { CipherCreateRequest } from "../models/request/cipher-create.request"; import { CipherCreateRequest } from "../models/request/cipher-create.request";
import { CipherPartialRequest } from "../models/request/cipher-partial.request"; import { CipherPartialRequest } from "../models/request/cipher-partial.request";
import { CipherRequest } from "../models/request/cipher.request"; import { CipherRequest } from "../models/request/cipher.request";
import { CipherView } from "../models/view/cipher.view";
import { CipherService } from "./cipher.service"; import { CipherService } from "./cipher.service";
const ENCRYPTED_TEXT = "This data has been encrypted";
const ENCRYPTED_BYTES = mock<EncArrayBuffer>();
const cipherData: CipherData = { const cipherData: CipherData = {
id: "id", id: "id",
organizationId: "orgId", organizationId: "orgId",
@ -35,6 +48,7 @@ const cipherData: CipherData = {
notes: "EncryptedString", notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z", creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null, deletedDate: null,
key: "EncKey",
reprompt: CipherRepromptType.None, reprompt: CipherRepromptType.None,
login: { login: {
uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }], uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }],
@ -88,6 +102,7 @@ describe("Cipher Service", () => {
const i18nService = mock<I18nService>(); const i18nService = mock<I18nService>();
const searchService = mock<SearchService>(); const searchService = mock<SearchService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
const configService = mock<ConfigServiceAbstraction>();
let cipherService: CipherService; let cipherService: CipherService;
let cipherObj: Cipher; let cipherObj: Cipher;
@ -101,6 +116,12 @@ describe("Cipher Service", () => {
mockReset(i18nService); mockReset(i18nService);
mockReset(searchService); mockReset(searchService);
mockReset(encryptService); mockReset(encryptService);
mockReset(configService);
encryptService.encryptToBytes.mockReturnValue(Promise.resolve(ENCRYPTED_BYTES));
encryptService.encrypt.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT)));
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
cipherService = new CipherService( cipherService = new CipherService(
cryptoService, cryptoService,
@ -110,7 +131,8 @@ describe("Cipher Service", () => {
searchService, searchService,
stateService, stateService,
encryptService, encryptService,
cipherFileUploadService cipherFileUploadService,
configService
); );
cipherObj = new Cipher(cipherData); cipherObj = new Cipher(cipherData);
@ -125,6 +147,12 @@ describe("Cipher Service", () => {
cryptoService.makeDataEncKey.mockReturnValue( cryptoService.makeDataEncKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32))) Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)))
); );
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false));
process.env.FLAGS = JSON.stringify({
enableCipherKeyEncryption: false,
});
const spy = jest.spyOn(cipherFileUploadService, "upload"); const spy = jest.spyOn(cipherFileUploadService, "upload");
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData); await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData);
@ -216,4 +244,68 @@ describe("Cipher Service", () => {
expect(spy).toHaveBeenCalledWith(cipherObj.id, expectedObj); expect(spy).toHaveBeenCalledWith(cipherObj.id, expectedObj);
}); });
}); });
describe("encrypt", () => {
let cipherView: CipherView;
beforeEach(() => {
cipherView = new CipherView();
cipherView.type = CipherType.Login;
encryptService.decryptToBytes.mockReturnValue(Promise.resolve(makeStaticByteArray(64)));
configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true));
cryptoService.makeCipherKey.mockReturnValue(
Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey)
);
cryptoService.encrypt.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT)));
});
describe("cipher.key", () => {
it("is null when enableCipherKeyEncryption flag is false", async () => {
process.env.FLAGS = JSON.stringify({
enableCipherKeyEncryption: false,
});
const cipher = await cipherService.encrypt(cipherView);
expect(cipher.key).toBeNull();
});
it("is defined when enableCipherKeyEncryption flag is true", async () => {
process.env.FLAGS = JSON.stringify({
enableCipherKeyEncryption: true,
});
const cipher = await cipherService.encrypt(cipherView);
expect(cipher.key).toBeDefined();
});
});
describe("encryptWithCipherKey", () => {
beforeEach(() => {
jest.spyOn<any, string>(cipherService, "encryptCipherWithCipherKey");
});
it("is not called when enableCipherKeyEncryption is false", async () => {
process.env.FLAGS = JSON.stringify({
enableCipherKeyEncryption: false,
});
await cipherService.encrypt(cipherView);
expect(cipherService["encryptCipherWithCipherKey"]).not.toHaveBeenCalled();
});
it("is called when enableCipherKeyEncryption is true", async () => {
process.env.FLAGS = JSON.stringify({
enableCipherKeyEncryption: true,
});
await cipherService.encrypt(cipherView);
expect(cipherService["encryptCipherWithCipherKey"]).toHaveBeenCalled();
});
});
});
}); });

View File

@ -1,13 +1,18 @@
import { firstValueFrom } from "rxjs";
import { SemVer } from "semver";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service"; import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service"; import { SettingsService } from "../../abstractions/settings.service";
import { FieldType, UriMatchType } from "../../enums"; import { FieldType, UriMatchType } from "../../enums";
import { ErrorResponse } from "../../models/response/error.response"; import { ErrorResponse } from "../../models/response/error.response";
import { View } from "../../models/view/view"; import { View } from "../../models/view/view";
import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service"; import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service"; import { StateService } from "../../platform/abstractions/state.service";
import { flagEnabled } from "../../platform/misc/flags";
import { sequentialize } from "../../platform/misc/sequentialize"; import { sequentialize } from "../../platform/misc/sequentialize";
import { Utils } from "../../platform/misc/utils"; import { Utils } from "../../platform/misc/utils";
import Domain from "../../platform/models/domain/domain-base"; import Domain from "../../platform/models/domain/domain-base";
@ -47,6 +52,8 @@ import { CipherView } from "../models/view/cipher.view";
import { FieldView } from "../models/view/field.view"; import { FieldView } from "../models/view/field.view";
import { PasswordHistoryView } from "../models/view/password-history.view"; import { PasswordHistoryView } from "../models/view/password-history.view";
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2023.9.1");
export class CipherService implements CipherServiceAbstraction { export class CipherService implements CipherServiceAbstraction {
private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache(
this.sortCiphersByLastUsed this.sortCiphersByLastUsed
@ -60,7 +67,8 @@ export class CipherService implements CipherServiceAbstraction {
private searchService: SearchService, private searchService: SearchService,
private stateService: StateService, private stateService: StateService,
private encryptService: EncryptService, private encryptService: EncryptService,
private cipherFileUploadService: CipherFileUploadService private cipherFileUploadService: CipherFileUploadService,
private configService: ConfigServiceAbstraction
) {} ) {}
async getDecryptedCipherCache(): Promise<CipherView[]> { async getDecryptedCipherCache(): Promise<CipherView[]> {
@ -85,63 +93,18 @@ export class CipherService implements CipherServiceAbstraction {
async encrypt( async encrypt(
model: CipherView, model: CipherView,
key?: SymmetricCryptoKey, keyForEncryption?: SymmetricCryptoKey,
keyForCipherKeyDecryption?: SymmetricCryptoKey,
originalCipher: Cipher = null originalCipher: Cipher = null
): Promise<Cipher> { ): Promise<Cipher> {
// Adjust password history
if (model.id != null) { if (model.id != null) {
if (originalCipher == null) { if (originalCipher == null) {
originalCipher = await this.get(model.id); originalCipher = await this.get(model.id);
} }
if (originalCipher != null) { if (originalCipher != null) {
const existingCipher = await originalCipher.decrypt(); await this.updateModelfromExistingCipher(model, originalCipher);
model.passwordHistory = existingCipher.passwordHistory || [];
if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) {
if (
existingCipher.login.password != null &&
existingCipher.login.password !== "" &&
existingCipher.login.password !== model.login.password
) {
const ph = new PasswordHistoryView();
ph.password = existingCipher.login.password;
ph.lastUsedDate = model.login.passwordRevisionDate = new Date();
model.passwordHistory.splice(0, 0, ph);
} else {
model.login.passwordRevisionDate = existingCipher.login.passwordRevisionDate;
}
}
if (existingCipher.hasFields) {
const existingHiddenFields = existingCipher.fields.filter(
(f) =>
f.type === FieldType.Hidden &&
f.name != null &&
f.name !== "" &&
f.value != null &&
f.value !== ""
);
const hiddenFields =
model.fields == null
? []
: model.fields.filter(
(f) => f.type === FieldType.Hidden && f.name != null && f.name !== ""
);
existingHiddenFields.forEach((ef) => {
const matchedField = hiddenFields.find((f) => f.name === ef.name);
if (matchedField == null || matchedField.value !== ef.value) {
const ph = new PasswordHistoryView();
ph.password = ef.name + ": " + ef.value;
ph.lastUsedDate = new Date();
model.passwordHistory.splice(0, 0, ph);
}
});
}
}
if (model.passwordHistory != null && model.passwordHistory.length === 0) {
model.passwordHistory = null;
} else if (model.passwordHistory != null && model.passwordHistory.length > 5) {
// only save last 5 history
model.passwordHistory = model.passwordHistory.slice(0, 5);
} }
this.adjustPasswordHistoryLength(model);
} }
const cipher = new Cipher(); const cipher = new Cipher();
@ -155,35 +118,32 @@ export class CipherService implements CipherServiceAbstraction {
cipher.reprompt = model.reprompt; cipher.reprompt = model.reprompt;
cipher.edit = model.edit; cipher.edit = model.edit;
if (key == null && cipher.organizationId != null) { if (await this.getCipherKeyEncryptionEnabled()) {
key = await this.cryptoService.getOrgKey(cipher.organizationId); cipher.key = originalCipher?.key ?? null;
if (key == null) { const userOrOrgKey = await this.getKeyForCipherKeyDecryption(cipher);
throw new Error("Cannot encrypt cipher for organization. No key."); // The keyForEncryption is only used for encrypting the cipher key, not the cipher itself, since cipher key encryption is enabled.
} // If the caller has provided a key for cipher key encryption, use it. Otherwise, use the user or org key.
} keyForEncryption ||= userOrOrgKey;
await Promise.all([ // If the caller has provided a key for cipher key decryption, use it. Otherwise, use the user or org key.
this.encryptObjProperty( keyForCipherKeyDecryption ||= userOrOrgKey;
return this.encryptCipherWithCipherKey(
model, model,
cipher, cipher,
{ keyForEncryption,
name: null, keyForCipherKeyDecryption
notes: null, );
}, } else {
key if (keyForEncryption == null && cipher.organizationId != null) {
), keyForEncryption = await this.cryptoService.getOrgKey(cipher.organizationId);
this.encryptCipherData(cipher, model, key), if (keyForEncryption == null) {
this.encryptFields(model.fields, key).then((fields) => { throw new Error("Cannot encrypt cipher for organization. No key.");
cipher.fields = fields; }
}), }
this.encryptPasswordHistories(model.passwordHistory, key).then((ph) => { // We want to ensure that the cipher key is null if cipher key encryption is disabled
cipher.passwordHistory = ph; // so that decryption uses the proper key.
}), cipher.key = null;
this.encryptAttachments(model.attachments, key).then((attachments) => { return this.encryptCipher(model, cipher, keyForEncryption);
cipher.attachments = attachments; }
}),
]);
return cipher;
} }
async encryptAttachments( async encryptAttachments(
@ -579,7 +539,7 @@ export class CipherService implements CipherServiceAbstraction {
cipher.organizationId = organizationId; cipher.organizationId = organizationId;
cipher.collectionIds = collectionIds; cipher.collectionIds = collectionIds;
const encCipher = await this.encrypt(cipher); const encCipher = await this.encryptSharedCipher(cipher);
const request = new CipherShareRequest(encCipher); const request = new CipherShareRequest(encCipher);
const response = await this.apiService.putShareCipher(cipher.id, request); const response = await this.apiService.putShareCipher(cipher.id, request);
const data = new CipherData(response, collectionIds); const data = new CipherData(response, collectionIds);
@ -597,7 +557,7 @@ export class CipherService implements CipherServiceAbstraction {
cipher.organizationId = organizationId; cipher.organizationId = organizationId;
cipher.collectionIds = collectionIds; cipher.collectionIds = collectionIds;
promises.push( promises.push(
this.encrypt(cipher).then((c) => { this.encryptSharedCipher(cipher).then((c) => {
encCiphers.push(c); encCiphers.push(c);
}) })
); );
@ -645,14 +605,29 @@ export class CipherService implements CipherServiceAbstraction {
data: Uint8Array, data: Uint8Array,
admin = false admin = false
): Promise<Cipher> { ): Promise<Cipher> {
let encKey: UserKey | OrgKey; const encKey = await this.getKeyForCipherKeyDecryption(cipher);
encKey = await this.cryptoService.getOrgKey(cipher.organizationId); const cipherKeyEncryptionEnabled = await this.getCipherKeyEncryptionEnabled();
encKey ||= await this.cryptoService.getUserKeyWithLegacySupport();
const dataEncKey = await this.cryptoService.makeDataEncKey(encKey); const cipherEncKey =
cipherKeyEncryptionEnabled && cipher.key != null
? (new SymmetricCryptoKey(
await this.encryptService.decryptToBytes(cipher.key, encKey)
) as UserKey)
: encKey;
const encFileName = await this.encryptService.encrypt(filename, encKey); //if cipher key encryption is disabled but the item has an individual key,
const encData = await this.encryptService.encryptToBytes(data, dataEncKey[0]); //then we rollback to using the user key as the main key of encryption of the item
//in order to keep item and it's attachments with the same encryption level
if (cipher.key != null && !cipherKeyEncryptionEnabled) {
const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher));
cipher = await this.encrypt(model);
await this.updateWithServer(cipher);
}
const encFileName = await this.encryptService.encrypt(filename, cipherEncKey);
const dataEncKey = await this.cryptoService.makeDataEncKey(cipherEncKey);
const encData = await this.encryptService.encryptToBytes(new Uint8Array(data), dataEncKey[0]);
const response = await this.cipherFileUploadService.upload( const response = await this.cipherFileUploadService.upload(
cipher, cipher,
@ -963,8 +938,80 @@ export class CipherService implements CipherServiceAbstraction {
await this.restore(restores); await this.restore(restores);
} }
async getKeyForCipherKeyDecryption(cipher: Cipher): Promise<UserKey | OrgKey> {
return (
(await this.cryptoService.getOrgKey(cipher.organizationId)) ||
((await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey)
);
}
// Helpers // Helpers
// In the case of a cipher that is being shared with an organization, we want to decrypt the
// cipher key with the user's key and then re-encrypt it with the organization's key.
private async encryptSharedCipher(model: CipherView): Promise<Cipher> {
const keyForCipherKeyDecryption = await this.cryptoService.getUserKeyWithLegacySupport();
return await this.encrypt(model, null, keyForCipherKeyDecryption);
}
private async updateModelfromExistingCipher(
model: CipherView,
originalCipher: Cipher
): Promise<void> {
const existingCipher = await originalCipher.decrypt(
await this.getKeyForCipherKeyDecryption(originalCipher)
);
model.passwordHistory = existingCipher.passwordHistory || [];
if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) {
if (
existingCipher.login.password != null &&
existingCipher.login.password !== "" &&
existingCipher.login.password !== model.login.password
) {
const ph = new PasswordHistoryView();
ph.password = existingCipher.login.password;
ph.lastUsedDate = model.login.passwordRevisionDate = new Date();
model.passwordHistory.splice(0, 0, ph);
} else {
model.login.passwordRevisionDate = existingCipher.login.passwordRevisionDate;
}
}
if (existingCipher.hasFields) {
const existingHiddenFields = existingCipher.fields.filter(
(f) =>
f.type === FieldType.Hidden &&
f.name != null &&
f.name !== "" &&
f.value != null &&
f.value !== ""
);
const hiddenFields =
model.fields == null
? []
: model.fields.filter(
(f) => f.type === FieldType.Hidden && f.name != null && f.name !== ""
);
existingHiddenFields.forEach((ef) => {
const matchedField = hiddenFields.find((f) => f.name === ef.name);
if (matchedField == null || matchedField.value !== ef.value) {
const ph = new PasswordHistoryView();
ph.password = ef.name + ": " + ef.value;
ph.lastUsedDate = new Date();
model.passwordHistory.splice(0, 0, ph);
}
});
}
}
private adjustPasswordHistoryLength(model: CipherView) {
if (model.passwordHistory != null && model.passwordHistory.length === 0) {
model.passwordHistory = null;
} else if (model.passwordHistory != null && model.passwordHistory.length > 5) {
// only save last 5 history
model.passwordHistory = model.passwordHistory.slice(0, 5);
}
}
private async shareAttachmentWithServer( private async shareAttachmentWithServer(
attachmentView: AttachmentView, attachmentView: AttachmentView,
cipherId: string, cipherId: string,
@ -1193,4 +1240,69 @@ export class CipherService implements CipherServiceAbstraction {
private clearSortedCiphers() { private clearSortedCiphers() {
this.sortedCiphersCache.clear(); this.sortedCiphersCache.clear();
} }
private async encryptCipher(
model: CipherView,
cipher: Cipher,
key: SymmetricCryptoKey
): Promise<Cipher> {
await Promise.all([
this.encryptObjProperty(
model,
cipher,
{
name: null,
notes: null,
},
key
),
this.encryptCipherData(cipher, model, key),
this.encryptFields(model.fields, key).then((fields) => {
cipher.fields = fields;
}),
this.encryptPasswordHistories(model.passwordHistory, key).then((ph) => {
cipher.passwordHistory = ph;
}),
this.encryptAttachments(model.attachments, key).then((attachments) => {
cipher.attachments = attachments;
}),
]);
return cipher;
}
private async encryptCipherWithCipherKey(
model: CipherView,
cipher: Cipher,
keyForCipherKeyEncryption: SymmetricCryptoKey,
keyForCipherKeyDecryption: SymmetricCryptoKey
): Promise<Cipher> {
// First, we get the key for cipher key encryption, in its decrypted form
let decryptedCipherKey: SymmetricCryptoKey;
if (cipher.key == null) {
decryptedCipherKey = await this.cryptoService.makeCipherKey();
} else {
decryptedCipherKey = new SymmetricCryptoKey(
await this.encryptService.decryptToBytes(cipher.key, keyForCipherKeyDecryption)
);
}
// Then, we have to encrypt the cipher key with the proper key.
cipher.key = await this.encryptService.encrypt(
decryptedCipherKey.key,
keyForCipherKeyEncryption
);
// Finally, we can encrypt the cipher with the decrypted cipher key.
return this.encryptCipher(model, cipher, decryptedCipherKey);
}
private async getCipherKeyEncryptionEnabled(): Promise<boolean> {
return (
flagEnabled("enableCipherKeyEncryption") &&
(await firstValueFrom(
this.configService.checkServerMeetsVersionRequirement$(CIPHER_KEY_ENC_MIN_SERVER_VER)
))
);
}
} }

View File

@ -258,12 +258,15 @@ export class VaultExportService implements VaultExportServiceAbstraction {
if (exportData.ciphers != null && exportData.ciphers.length > 0) { if (exportData.ciphers != null && exportData.ciphers.length > 0) {
exportData.ciphers exportData.ciphers
.filter((c) => c.deletedDate === null) .filter((c) => c.deletedDate === null)
.forEach((c) => { .forEach(async (c) => {
const cipher = new Cipher(new CipherData(c)); const cipher = new Cipher(new CipherData(c));
exportPromises.push( exportPromises.push(
cipher.decrypt().then((decCipher) => { this.cipherService
decCiphers.push(decCipher); .getKeyForCipherKeyDecryption(cipher)
}) .then((key) => cipher.decrypt(key))
.then((decCipher) => {
decCiphers.push(decCipher);
})
); );
}); });
} }

View File

@ -4,6 +4,7 @@ import { KdfType } from "@bitwarden/common/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { import {
BitwardenPasswordProtectedImporter, BitwardenPasswordProtectedImporter,
@ -17,6 +18,7 @@ describe("BitwardenPasswordProtectedImporter", () => {
let importer: BitwardenPasswordProtectedImporter; let importer: BitwardenPasswordProtectedImporter;
let cryptoService: MockProxy<CryptoService>; let cryptoService: MockProxy<CryptoService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
let cipherService: MockProxy<CipherService>;
const password = Utils.newGuid(); const password = Utils.newGuid();
const promptForPassword_callback = async () => { const promptForPassword_callback = async () => {
return password; return password;
@ -25,10 +27,12 @@ describe("BitwardenPasswordProtectedImporter", () => {
beforeEach(() => { beforeEach(() => {
cryptoService = mock<CryptoService>(); cryptoService = mock<CryptoService>();
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
cipherService = mock<CipherService>();
importer = new BitwardenPasswordProtectedImporter( importer = new BitwardenPasswordProtectedImporter(
cryptoService, cryptoService,
i18nService, i18nService,
cipherService,
promptForPassword_callback promptForPassword_callback
); );
}); });

View File

@ -6,6 +6,7 @@ import {
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { import {
@ -25,7 +26,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
protected constructor( protected constructor(
protected cryptoService: CryptoService, protected cryptoService: CryptoService,
protected i18nService: I18nService protected i18nService: I18nService,
protected cipherService: CipherService
) { ) {
super(); super();
} }
@ -96,7 +98,9 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
}); });
} }
const view = await cipher.decrypt(); const view = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher)
);
this.cleanupCipher(view); this.cleanupCipher(view);
this.result.ciphers.push(view); this.result.ciphers.push(view);
} }

View File

@ -4,21 +4,24 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { BitwardenPasswordProtectedFileFormat } from "@bitwarden/exporter/vault-export/bitwarden-json-export-types"; import { BitwardenPasswordProtectedFileFormat } from "@bitwarden/exporter/vault-export/bitwarden-json-export-types";
import { ImportResult } from "../../models/import-result"; import { ImportResult } from "../../models/import-result";
import { Importer } from "../importer"; import { Importer } from "../importer";
import { BitwardenJsonImporter } from "./bitwarden-json-importer"; import { BitwardenJsonImporter } from "./bitwarden-json-importer";
export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer { export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer {
private key: SymmetricCryptoKey; private key: SymmetricCryptoKey;
constructor( constructor(
cryptoService: CryptoService, cryptoService: CryptoService,
i18nService: I18nService, i18nService: I18nService,
cipherService: CipherService,
private promptForPassword_callback: () => Promise<string> private promptForPassword_callback: () => Promise<string>
) { ) {
super(cryptoService, i18nService); super(cryptoService, i18nService, cipherService);
} }
async parse(data: string): Promise<ImportResult> { async parse(data: string): Promise<ImportResult> {

View File

@ -203,6 +203,7 @@ export class ImportService implements ImportServiceAbstraction {
return new BitwardenPasswordProtectedImporter( return new BitwardenPasswordProtectedImporter(
this.cryptoService, this.cryptoService,
this.i18nService, this.i18nService,
this.cipherService,
promptForPassword_callback promptForPassword_callback
); );
case "lastpasscsv": case "lastpasscsv":