From 8bef0883f54bb0c8c8ac7017cad8f2c4c835cc69 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 28 Sep 2023 08:44:57 -0400 Subject: [PATCH] Individual Vault Item Encryption Feature (#6241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 56a590c4919f119cb1465eb7091a4384f5d90699. * 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 * 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 * 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 * 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 * 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 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 as the return values are not being used. * PM-3169 - StateSvc - Per PR feedback, update setUserSsoOrganizationIdentifier signature to explicitly use null instead of partial 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 * [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 --------- Co-authored-by: Jake Fink * 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 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 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 6ec22f95702050c12716f79c7d7454835f9b2807. * 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 * 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 Co-authored-by: Matt Gibson * 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: https://github.com/bitwarden/clients/commit/55a797d4ff571a1942686a32fdcbb5ad0311b5ae * 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 Co-authored-by: gbubemismith Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Co-authored-by: Jacob Fink Co-authored-by: Matt Gibson Co-authored-by: Andreas Coroiu Co-authored-by: Carlos Gonçalves Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Co-authored-by: Andreas Coroiu Co-authored-by: Andreas Coroiu Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: André Bispo Co-authored-by: Thomas Rittson Co-authored-by: Vincent Salucci Co-authored-by: Robyn MacCallum Co-authored-by: Jonathan Prusik Co-authored-by: Matt Gibson Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- apps/browser/config/base.json | 3 +- apps/browser/config/development.json | 3 +- apps/browser/config/production.json | 4 +- .../background/notification.background.ts | 2 +- .../browser/src/background/main.background.ts | 74 ++--- .../config-api.service.factory.ts | 32 ++ .../config-service.factory.ts | 49 +++ .../cipher-service.factory.ts | 12 +- apps/cli/config/development.json | 4 +- apps/cli/config/production.json | 4 +- .../admin-console/commands/share.command.ts | 8 +- apps/cli/src/bw.ts | 58 ++-- apps/cli/src/commands/edit.command.ts | 12 +- apps/cli/src/commands/get.command.ts | 4 +- .../platform/services/cli-config.service.ts | 9 + apps/cli/src/vault/create.command.ts | 8 +- apps/desktop/config/base.json | 3 +- apps/desktop/config/development.json | 3 +- apps/desktop/config/production.json | 3 +- apps/desktop/src/locales/en/messages.json | 2 +- .../encrypted-message-handler.service.ts | 4 +- apps/web/config/base.json | 3 +- apps/web/config/cloud.json | 3 +- apps/web/config/development.json | 3 +- apps/web/config/euprd.json | 3 +- apps/web/config/qa.json | 3 +- apps/web/config/selfhosted.json | 3 +- .../app/vault/org-vault/add-edit.component.ts | 2 +- .../components/collections.component.ts | 4 +- .../angular/src/components/share.component.ts | 8 +- .../src/services/jslib-services.module.ts | 7 +- .../vault/components/add-edit.component.ts | 4 +- .../vault/components/attachments.component.ts | 12 +- .../components/password-history.component.ts | 4 +- .../src/vault/components/view.component.ts | 4 +- .../common/src/models/export/cipher.export.ts | 3 + .../config/config.service.abstraction.ts | 4 + .../platform/abstractions/crypto.service.ts | 6 + .../interfaces/decryptable.interface.ts | 2 +- libs/common/src/platform/misc/flags.ts | 1 + .../models/domain/symmetric-crypto-key.ts | 1 + .../services/config/config.service.ts | 25 +- .../src/platform/services/crypto.service.ts | 6 + .../src/vault/abstractions/cipher.service.ts | 4 +- .../src/vault/models/data/cipher.data.ts | 2 + .../src/vault/models/domain/cipher.spec.ts | 79 ++++- libs/common/src/vault/models/domain/cipher.ts | 24 +- .../vault/models/request/cipher.request.ts | 2 + .../vault/models/response/cipher.response.ts | 2 + .../src/vault/services/cipher.service.spec.ts | 98 +++++- .../src/vault/services/cipher.service.ts | 282 ++++++++++++------ .../services/vault-export.service.ts | 11 +- ...warden-password-protected-importer.spec.ts | 4 + .../bitwarden/bitwarden-json-importer.ts | 8 +- .../bitwarden-password-protected-importer.ts | 5 +- libs/importer/src/services/import.service.ts | 1 + 56 files changed, 729 insertions(+), 205 deletions(-) create mode 100644 apps/browser/src/platform/background/service-factories/config-api.service.factory.ts create mode 100644 apps/browser/src/platform/background/service-factories/config-service.factory.ts create mode 100644 apps/cli/src/platform/services/cli-config.service.ts diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json index 81b11cd38b..348f00d1f3 100644 --- a/apps/browser/config/base.json +++ b/apps/browser/config/base.json @@ -1,6 +1,7 @@ { "dev_flags": {}, "flags": { - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index 972812a9c5..eafd0ffd87 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -6,6 +6,7 @@ } }, "flags": { - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/browser/config/production.json b/apps/browser/config/production.json index b04d1531a2..f57c3d9bc3 100644 --- a/apps/browser/config/production.json +++ b/apps/browser/config/production.json @@ -1,3 +1,5 @@ { - "flags": {} + "flags": { + "enableCipherKeyEncryption": false + } } diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 1cb006fa3a..73bdc2cd16 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -464,7 +464,7 @@ export default class NotificationBackground { private async getDecryptedCipherById(cipherId: string) { const cipher = await this.cipherService.get(cipherId); if (cipher != null && cipher.type === CipherType.Login) { - return await cipher.decrypt(); + return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); } return null; } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3aba0c7767..7a9863f3a7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -322,23 +322,6 @@ export default class MainBackground { ); 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.cryptoService, this.i18nService, @@ -362,14 +345,6 @@ export default class MainBackground { this.cryptoFunctionService, logoutCallback ); - this.vaultFilterService = new VaultFilterService( - this.stateService, - this.organizationService, - this.folderService, - this.cipherService, - this.collectionService, - this.policyService - ); this.passwordStrengthService = new PasswordStrengthService(); @@ -436,6 +411,36 @@ export default class MainBackground { 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.cryptoService, this.tokenService, @@ -444,6 +449,15 @@ export default class MainBackground { this.userVerificationService ); + this.vaultFilterService = new VaultFilterService( + this.stateService, + this.organizationService, + this.folderService, + this.cipherService, + this.collectionService, + this.policyService + ); + this.vaultTimeoutService = new VaultTimeoutService( this.cipherService, this.folderService, @@ -533,16 +547,6 @@ export default class MainBackground { 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(); const systemUtilsServiceReloadCallback = () => { diff --git a/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts b/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts new file mode 100644 index 0000000000..e9e1b86488 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/config-api.service.factory.ts @@ -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 { + return factory( + cache, + "configApiService", + opts, + async () => + new ConfigApiService( + await apiServiceFactory(cache, opts), + await authServiceFactory(cache, opts) + ) + ); +} diff --git a/apps/browser/src/platform/background/service-factories/config-service.factory.ts b/apps/browser/src/platform/background/service-factories/config-service.factory.ts new file mode 100644 index 0000000000..a5dc6016c6 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/config-service.factory.ts @@ -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 { + 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 + ) + ); +} diff --git a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts index 006daadc1a..46062ebc9c 100644 --- a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts @@ -18,8 +18,12 @@ import { ApiServiceInitOptions, } from "../../../platform/background/service-factories/api-service.factory"; import { - CryptoServiceInitOptions, + configServiceFactory, + ConfigServiceInitOptions, +} from "../../../platform/background/service-factories/config-service.factory"; +import { cryptoServiceFactory, + CryptoServiceInitOptions, } from "../../../platform/background/service-factories/crypto-service.factory"; import { EncryptServiceInitOptions, @@ -49,7 +53,8 @@ export type CipherServiceInitOptions = CipherServiceFactoryOptions & I18nServiceInitOptions & SearchServiceInitOptions & StateServiceInitOptions & - EncryptServiceInitOptions; + EncryptServiceInitOptions & + ConfigServiceInitOptions; export function cipherServiceFactory( cache: { cipherService?: AbstractCipherService } & CachedServices, @@ -68,7 +73,8 @@ export function cipherServiceFactory( await searchServiceFactory(cache, opts), await stateServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), - await cipherFileUploadServiceFactory(cache, opts) + await cipherFileUploadServiceFactory(cache, opts), + await configServiceFactory(cache, opts) ) ); } diff --git a/apps/cli/config/development.json b/apps/cli/config/development.json index b04d1531a2..f57c3d9bc3 100644 --- a/apps/cli/config/development.json +++ b/apps/cli/config/development.json @@ -1,3 +1,5 @@ { - "flags": {} + "flags": { + "enableCipherKeyEncryption": false + } } diff --git a/apps/cli/config/production.json b/apps/cli/config/production.json index b04d1531a2..f57c3d9bc3 100644 --- a/apps/cli/config/production.json +++ b/apps/cli/config/production.json @@ -1,3 +1,5 @@ { - "flags": {} + "flags": { + "enableCipherKeyEncryption": false + } } diff --git a/apps/cli/src/admin-console/commands/share.command.ts b/apps/cli/src/admin-console/commands/share.command.ts index 68bd8a1805..88fe256af1 100644 --- a/apps/cli/src/admin-console/commands/share.command.ts +++ b/apps/cli/src/admin-console/commands/share.command.ts @@ -45,11 +45,15 @@ export class ShareCommand { if (cipher.organizationId != null) { 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 { await this.cipherService.shareWithServer(cipherView, organizationId, req); 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); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 42ba158ee7..ca50169019 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -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 { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; 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 { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.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 { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -75,6 +77,7 @@ import { } from "@bitwarden/importer"; 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 { ConsoleLogService } from "./platform/services/console-log.service"; import { I18nService } from "./platform/services/i18n.service"; @@ -147,6 +150,8 @@ export class Main { devicesApiService: DevicesApiServiceAbstraction; deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; authRequestCryptoService: AuthRequestCryptoServiceAbstraction; + configApiService: ConfigApiServiceAbstraction; + configService: CliConfigService; constructor() { let p = null; @@ -252,28 +257,8 @@ export class Main { 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.folderService = new FolderService( - this.cryptoService, - this.i18nService, - this.cipherService, - this.stateService - ); - - this.folderApiService = new FolderApiService(this.folderService, this.apiService); - this.collectionService = new CollectionService( this.cryptoService, this.i18nService, @@ -349,6 +334,38 @@ export class Main { 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) => await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto); @@ -472,6 +489,7 @@ export class Main { const locale = await this.stateService.getLocale(); await this.i18nService.init(locale); this.twoFactorService.init(); + this.configService.init(); const installedVersion = await this.stateService.getInstalledVersion(); const currentVersion = await this.platformUtilsService.getApplicationVersion(); diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 960b099914..60e5ee7936 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -77,7 +77,9 @@ export class EditCommand { return Response.notFound(); } - let cipherView = await cipher.decrypt(); + let cipherView = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); if (cipherView.isDeleted) { return Response.badRequest("You may not edit a deleted item. Use the restore command first."); } @@ -86,7 +88,9 @@ export class EditCommand { try { await this.cipherService.updateWithServer(encCipher); 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); return Response.success(res); } catch (e) { @@ -109,7 +113,9 @@ export class EditCommand { try { await this.cipherService.saveCollectionsWithServer(cipher); 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); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 265e24c9d4..4a84f1efef 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -103,7 +103,9 @@ export class GetCommand extends DownloadCommand { if (Utils.isGuid(id)) { const cipher = await this.cipherService.get(id); if (cipher != null) { - decCipher = await cipher.decrypt(); + decCipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); } } else if (id.trim() !== "") { let ciphers = await this.cipherService.getAllDecrypted(); diff --git a/apps/cli/src/platform/services/cli-config.service.ts b/apps/cli/src/platform/services/cli-config.service.ts new file mode 100644 index 0000000000..6faa1b12e8 --- /dev/null +++ b/apps/cli/src/platform/services/cli-config.service.ts @@ -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; +} diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 3f4d53e973..01217dbc30 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -80,7 +80,9 @@ export class CreateCommand { try { await this.cipherService.createWithServer(cipher); 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); return Response.success(res); } catch (e) { @@ -141,7 +143,9 @@ export class CreateCommand { new Uint8Array(fileBuf).buffer ); 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)); } catch (e) { return Response.error(e); diff --git a/apps/desktop/config/base.json b/apps/desktop/config/base.json index 3ae895a19c..cb408a87d8 100644 --- a/apps/desktop/config/base.json +++ b/apps/desktop/config/base.json @@ -1,6 +1,7 @@ { "dev_flags": {}, "flags": { - "multithreadDecryption": false + "multithreadDecryption": false, + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/config/development.json b/apps/desktop/config/development.json index b587e9ecfb..d2b1073812 100644 --- a/apps/desktop/config/development.json +++ b/apps/desktop/config/development.json @@ -1,6 +1,7 @@ { "devFlags": {}, "flags": { - "showDDGSetting": true + "showDDGSetting": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/config/production.json b/apps/desktop/config/production.json index 56f1934130..39b78094d0 100644 --- a/apps/desktop/config/production.json +++ b/apps/desktop/config/production.json @@ -1,5 +1,6 @@ { "flags": { - "showDDGSetting": true + "showDDGSetting": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 48e3f0e7bd..d3a13313e9 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1,4 +1,4 @@ -{ +{ "bitwarden": { "message": "Bitwarden" }, diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index 1889bfc745..1ce52cfecc 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -190,7 +190,9 @@ export class EncryptedMessageHandlerService { if (cipher === null) { return { status: "failure" }; } - const cipherView = await cipher.decrypt(); + const cipherView = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher) + ); cipherView.name = credentialUpdatePayload.name; cipherView.login.password = credentialUpdatePayload.password; cipherView.login.username = credentialUpdatePayload.userName; diff --git a/apps/web/config/base.json b/apps/web/config/base.json index ed0bc0a850..a377298c63 100644 --- a/apps/web/config/base.json +++ b/apps/web/config/base.json @@ -12,6 +12,7 @@ }, "flags": { "secretsManager": false, - "showPasswordless": false + "showPasswordless": false, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json index 45269d18c6..6e5c65af1d 100644 --- a/apps/web/config/cloud.json +++ b/apps/web/config/cloud.json @@ -18,6 +18,7 @@ }, "flags": { "secretsManager": true, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/development.json b/apps/web/config/development.json index 7aeffe55d0..e3107f8788 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -11,6 +11,7 @@ }, "flags": { "secretsManager": true, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/euprd.json b/apps/web/config/euprd.json index 19864cdab6..4b6c9fa909 100644 --- a/apps/web/config/euprd.json +++ b/apps/web/config/euprd.json @@ -12,6 +12,7 @@ }, "flags": { "secretsManager": true, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index 6f51407901..be9911cc57 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -12,6 +12,7 @@ }, "flags": { "secretsManager": true, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json index 8ffbc0f9e5..6b290baa6b 100644 --- a/apps/web/config/selfhosted.json +++ b/apps/web/config/selfhosted.json @@ -8,6 +8,7 @@ }, "flags": { "secretsManager": false, - "showPasswordless": true + "showPasswordless": true, + "enableCipherKeyEncryption": false } } diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 46d8440d48..bea55856a4 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -110,7 +110,7 @@ export class AddEditComponent extends BaseAddEditComponent { if (!this.organization.canEditAnyCollection) { 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() { diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 2fa5b5d3d3..945a79d1d7 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -37,7 +37,9 @@ export class CollectionsComponent implements OnInit { async load() { this.cipherDomain = await this.loadCipher(); 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.forEach((c) => ((c as any).checked = false)); diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 73792f91fe..6d0764be9b 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -66,7 +66,9 @@ export class ShareComponent implements OnInit, OnDestroy { }); 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(); } @@ -94,7 +96,9 @@ export class ShareComponent implements OnInit, OnDestroy { } 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 orgName = orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 02f6278fec..5c19d0fe77 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -271,7 +271,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; searchService: SearchServiceAbstraction, stateService: StateServiceAbstraction, encryptService: EncryptService, - fileUploadService: CipherFileUploadServiceAbstraction + fileUploadService: CipherFileUploadServiceAbstraction, + configService: ConfigServiceAbstraction ) => new CipherService( cryptoService, @@ -281,7 +282,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; searchService, stateService, encryptService, - fileUploadService + fileUploadService, + configService ), deps: [ CryptoServiceAbstraction, @@ -292,6 +294,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; StateServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, + ConfigServiceAbstraction, ], }, { diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 377fe88b63..3aca51e44a 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -225,7 +225,9 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.cipher == null) { if (this.editMode) { 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 if (this.cloneMode) { diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index c7a8dd2ee2..e1974e2b7d 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -73,7 +73,9 @@ export class AttachmentsComponent implements OnInit { try { this.formPromise = this.saveCipherAttachment(files[0]); 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.onUploadedAttachment.emit(); } catch (e) { @@ -179,7 +181,9 @@ export class AttachmentsComponent implements OnInit { protected async init() { 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(); this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; @@ -229,7 +233,9 @@ export class AttachmentsComponent implements OnInit { decBuf, admin ); - this.cipher = await this.cipherDomain.decrypt(); + this.cipher = await this.cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain) + ); // 3. Delete old this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 8fbea9d8fb..3a25b6930a 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -33,7 +33,9 @@ export class PasswordHistoryComponent implements OnInit { protected async init() { 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; } } diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 18ea5474fd..40523afd85 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -113,7 +113,9 @@ export class ViewComponent implements OnDestroy, OnInit { this.cleanUp(); 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.showPremiumRequiredTotp = this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 342cf59fd8..3ae6c9757d 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -88,6 +88,7 @@ export class CipherExport { domain.notes = req.notes != null ? new EncString(req.notes) : null; domain.favorite = req.favorite; domain.reprompt = req.reprompt ?? CipherRepromptType.None; + domain.key = req.key != null ? new EncString(req.key) : null; if (req.fields != null) { domain.fields = req.fields.map((f) => FieldExport.toDomain(f)); @@ -135,6 +136,7 @@ export class CipherExport { revisionDate: Date = null; creationDate: 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 build(o: CipherView | CipherDomain) { @@ -149,6 +151,7 @@ export class CipherExport { } else { this.name = o.name?.encryptedString; this.notes = o.notes?.encryptedString; + this.key = o.key?.encryptedString; } this.favorite = o.favorite; diff --git a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config.service.abstraction.ts index 59f87b0fa2..67f7f2f4ce 100644 --- a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/config/config.service.abstraction.ts @@ -1,4 +1,5 @@ import { Observable } from "rxjs"; +import { SemVer } from "semver"; import { FeatureFlag } from "../../../enums/feature-flag.enum"; import { Region } from "../environment.service"; @@ -16,6 +17,9 @@ export abstract class ConfigServiceAbstraction { key: FeatureFlag, defaultValue?: T ) => Promise; + checkServerMeetsVersionRequirement$: ( + minimumRequiredServerVersion: SemVer + ) => Observable; /** * Force ConfigService to fetch an updated config from the server and emit it from serverConfig$ diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index a868484bd0..8f1d9c4866 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -6,6 +6,7 @@ import { KeySuffixOptions, KdfType, HashPurpose } from "../../enums"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { + CipherKey, MasterKey, OrgKey, PinKey, @@ -372,6 +373,11 @@ export abstract class CryptoService { */ rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise; randomNumber: (min: number, max: number) => Promise; + /** + * Generates a new cipher key + * @returns A new cipher key + */ + makeCipherKey: () => Promise; /** * Initialize all necessary crypto keys needed for a new account. diff --git a/libs/common/src/platform/interfaces/decryptable.interface.ts b/libs/common/src/platform/interfaces/decryptable.interface.ts index ae5e8ebbf8..35895bfd6f 100644 --- a/libs/common/src/platform/interfaces/decryptable.interface.ts +++ b/libs/common/src/platform/interfaces/decryptable.interface.ts @@ -8,5 +8,5 @@ import { InitializerMetadata } from "./initializer-metadata.interface"; * @example Cipher implements Decryptable */ export interface Decryptable extends InitializerMetadata { - decrypt: (key?: SymmetricCryptoKey) => Promise; + decrypt: (key: SymmetricCryptoKey) => Promise; } diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index c1bdafaa0f..5360950567 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -3,6 +3,7 @@ export type SharedFlags = { multithreadDecryption: boolean; showPasswordless?: boolean; + enableCipherKeyEncryption?: boolean; }; // required to avoid linting errors when there are no flags diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index 818155ef98..8f3b46f077 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -83,3 +83,4 @@ export type MasterKey = Opaque; export type PinKey = Opaque; export type OrgKey = Opaque; export type ProviderKey = Opaque; +export type CipherKey = Opaque; diff --git a/libs/common/src/platform/services/config/config.service.ts b/libs/common/src/platform/services/config/config.service.ts index 008f5a764d..45db66af0e 100644 --- a/libs/common/src/platform/services/config/config.service.ts +++ b/libs/common/src/platform/services/config/config.service.ts @@ -10,6 +10,7 @@ import { merge, timer, } from "rxjs"; +import { SemVer } from "semver"; import { AuthService } from "../../../auth/abstractions/auth.service"; 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; export class ConfigService implements ConfigServiceAbstraction { + private inited = false; + protected _serverConfig = new ReplaySubject(1); serverConfig$ = this._serverConfig.asObservable(); + private _forceFetchConfig = new Subject(); - private inited = false; + protected refreshTimer$ = timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS); // after 1 hour, then every hour cloudRegion$ = this.serverConfig$.pipe( 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 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._forceFetchConfig // manual ) @@ -103,4 +107,21 @@ export class ConfigService implements ConfigServiceAbstraction { await this.stateService.setServerConfig(data); 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; + }) + ); + } } diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 3b4090ef34..0ea9acc53d 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -27,6 +27,7 @@ import { EFFLongWordList } from "../misc/wordlist"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { + CipherKey, MasterKey, OrgKey, PinKey, @@ -596,6 +597,11 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(sendKey); } + async makeCipherKey(): Promise { + const randomBytes = await this.cryptoFunctionService.aesGenerateKey(512); + return new SymmetricCryptoKey(randomBytes) as CipherKey; + } + async clearKeys(userId?: string): Promise { await this.clearUserKey(true, userId); await this.clearMasterKeyHash(userId); diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index e328c5bd49..404a58abb1 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -11,7 +11,8 @@ export abstract class CipherService { clearCache: (userId?: string) => Promise; encrypt: ( model: CipherView, - key?: SymmetricCryptoKey, + keyForEncryption?: SymmetricCryptoKey, + keyForCipherKeyDecryption?: SymmetricCryptoKey, originalCipher?: Cipher ) => Promise; encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; @@ -81,4 +82,5 @@ export abstract class CipherService { organizationId?: string, asAdmin?: boolean ) => Promise; + getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise; } diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index 2f83ee194b..1452ffe7ee 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -33,6 +33,7 @@ export class CipherData { creationDate: string; deletedDate: string; reprompt: CipherRepromptType; + key: string; constructor(response?: CipherResponse, collectionIds?: string[]) { if (response == null) { @@ -54,6 +55,7 @@ export class CipherData { this.creationDate = response.creationDate; this.deletedDate = response.deletedDate; this.reprompt = response.reprompt; + this.key = response.key; switch (this.type) { case CipherType.Login: diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index d5c141487a..6234fe7029 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -2,10 +2,14 @@ import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { Jsonify } from "type-fest"; -import { mockEnc, mockFromJson } from "../../../../spec"; +import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; 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 { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; +import { CipherService } from "../../abstractions/cipher.service"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherData } from "../../models/data/cipher.data"; @@ -47,6 +51,7 @@ describe("Cipher DTO", () => { attachments: null, fields: null, passwordHistory: null, + key: null, }); }); @@ -69,6 +74,7 @@ describe("Cipher DTO", () => { creationDate: "2022-01-01T12:00:00.000Z", deletedDate: null, reprompt: CipherRepromptType.None, + key: "EncryptedString", login: { uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }], username: "EncryptedString", @@ -136,6 +142,7 @@ describe("Cipher DTO", () => { creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: null, reprompt: 0, + key: { encryptedString: "EncryptedString", encryptionType: 0 }, login: { passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"), autofillOnPageLoad: false, @@ -206,6 +213,7 @@ describe("Cipher DTO", () => { cipher.creationDate = new Date("2022-01-01T12:00:00.000Z"); cipher.deletedDate = null; cipher.reprompt = CipherRepromptType.None; + cipher.key = mockEnc("EncKey"); const loginView = new LoginView(); loginView.username = "username"; @@ -215,7 +223,20 @@ describe("Cipher DTO", () => { login.decrypt(Arg.any(), Arg.any()).resolves(loginView); cipher.login = login; - const cipherView = await cipher.decrypt(); + const cryptoService = Substitute.for(); + const encryptService = Substitute.for(); + const cipherService = Substitute.for(); + + 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({ id: "id", @@ -261,6 +282,7 @@ describe("Cipher DTO", () => { creationDate: "2022-01-01T12:00:00.000Z", deletedDate: null, reprompt: CipherRepromptType.None, + key: "EncKey", secureNote: { type: SecureNoteType.Generic, }, @@ -292,6 +314,7 @@ describe("Cipher DTO", () => { attachments: null, fields: null, passwordHistory: null, + key: { encryptedString: "EncKey", encryptionType: 0 }, }); }); @@ -318,8 +341,22 @@ describe("Cipher DTO", () => { cipher.reprompt = CipherRepromptType.None; cipher.secureNote = new SecureNote(); cipher.secureNote.type = SecureNoteType.Generic; + cipher.key = mockEnc("EncKey"); - const cipherView = await cipher.decrypt(); + const cryptoService = Substitute.for(); + const encryptService = Substitute.for(); + const cipherService = Substitute.for(); + + 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({ id: "id", @@ -373,6 +410,7 @@ describe("Cipher DTO", () => { expYear: "EncryptedString", code: "EncryptedString", }, + key: "EncKey", }; }); @@ -408,6 +446,7 @@ describe("Cipher DTO", () => { attachments: null, fields: 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.deletedDate = null; cipher.reprompt = CipherRepromptType.None; + cipher.key = mockEnc("EncKey"); const cardView = new CardView(); cardView.cardholderName = "cardholderName"; @@ -441,7 +481,20 @@ describe("Cipher DTO", () => { card.decrypt(Arg.any(), Arg.any()).resolves(cardView); cipher.card = card; - const cipherView = await cipher.decrypt(); + const cryptoService = Substitute.for(); + const encryptService = Substitute.for(); + const cipherService = Substitute.for(); + + 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({ id: "id", @@ -487,6 +540,7 @@ describe("Cipher DTO", () => { creationDate: "2022-01-01T12:00:00.000Z", deletedDate: null, reprompt: CipherRepromptType.None, + key: "EncKey", identity: { title: "EncryptedString", firstName: "EncryptedString", @@ -554,6 +608,7 @@ describe("Cipher DTO", () => { attachments: null, fields: 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.deletedDate = null; cipher.reprompt = CipherRepromptType.None; + cipher.key = mockEnc("EncKey"); const identityView = new IdentityView(); identityView.firstName = "firstName"; @@ -587,7 +643,20 @@ describe("Cipher DTO", () => { identity.decrypt(Arg.any(), Arg.any()).resolves(identityView); cipher.identity = identity; - const cipherView = await cipher.decrypt(); + const cryptoService = Substitute.for(); + const encryptService = Substitute.for(); + const cipherService = Substitute.for(); + + 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({ id: "id", diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index f85b6cb45c..23349695a7 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -1,6 +1,7 @@ import { Jsonify } from "type-fest"; import { Decryptable } from "../../../platform/interfaces/decryptable.interface"; +import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -45,6 +46,7 @@ export class Cipher extends Domain implements Decryptable { creationDate: Date; deletedDate: Date; reprompt: CipherRepromptType; + key: EncString; constructor(obj?: CipherData, localData: LocalData = null) { super(); @@ -61,6 +63,7 @@ export class Cipher extends Domain implements Decryptable { folderId: null, name: null, notes: null, + key: null, }, ["id", "organizationId", "folderId"] ); @@ -117,9 +120,17 @@ export class Cipher extends Domain implements Decryptable { } } - async decrypt(encKey?: SymmetricCryptoKey): Promise { + // 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 { 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( model, { @@ -147,14 +158,12 @@ export class Cipher extends Domain implements Decryptable { break; } - const orgId = this.organizationId; - if (this.attachments != null && this.attachments.length > 0) { const attachments: any[] = []; await this.attachments.reduce((promise, attachment) => { return promise .then(() => { - return attachment.decrypt(orgId, encKey); + return attachment.decrypt(this.organizationId, encKey); }) .then((decAttachment) => { attachments.push(decAttachment); @@ -168,7 +177,7 @@ export class Cipher extends Domain implements Decryptable { await this.fields.reduce((promise, field) => { return promise .then(() => { - return field.decrypt(orgId, encKey); + return field.decrypt(this.organizationId, encKey); }) .then((decField) => { fields.push(decField); @@ -182,7 +191,7 @@ export class Cipher extends Domain implements Decryptable { await this.passwordHistory.reduce((promise, ph) => { return promise .then(() => { - return ph.decrypt(orgId, encKey); + return ph.decrypt(this.organizationId, encKey); }) .then((decPh) => { passwordHistory.push(decPh); @@ -209,6 +218,7 @@ export class Cipher extends Domain implements Decryptable { c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null; c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null; c.reprompt = this.reprompt; + c.key = this.key?.encryptedString; this.buildDataModel(this, c, { name: null, @@ -257,6 +267,7 @@ export class Cipher extends Domain implements Decryptable { const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a)); const fields = obj.fields?.map((f: any) => Field.fromJSON(f)); const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph)); + const key = EncString.fromJSON(obj.key); Object.assign(domain, obj, { name, @@ -266,6 +277,7 @@ export class Cipher extends Domain implements Decryptable { attachments, fields, passwordHistory, + key, }); switch (obj.type) { diff --git a/libs/common/src/vault/models/request/cipher.request.ts b/libs/common/src/vault/models/request/cipher.request.ts index cae48fb7af..0f34200e79 100644 --- a/libs/common/src/vault/models/request/cipher.request.ts +++ b/libs/common/src/vault/models/request/cipher.request.ts @@ -29,6 +29,7 @@ export class CipherRequest { attachments2: { [id: string]: AttachmentRequest }; lastKnownRevisionDate: Date; reprompt: CipherRepromptType; + key: string; constructor(cipher: Cipher) { this.type = cipher.type; @@ -39,6 +40,7 @@ export class CipherRequest { this.favorite = cipher.favorite; this.lastKnownRevisionDate = cipher.revisionDate; this.reprompt = cipher.reprompt; + this.key = cipher.key?.encryptedString; switch (this.type) { case CipherType.Login: diff --git a/libs/common/src/vault/models/response/cipher.response.ts b/libs/common/src/vault/models/response/cipher.response.ts index 71e4337377..8bc8a37874 100644 --- a/libs/common/src/vault/models/response/cipher.response.ts +++ b/libs/common/src/vault/models/response/cipher.response.ts @@ -32,6 +32,7 @@ export class CipherResponse extends BaseResponse { creationDate: string; deletedDate: string; reprompt: CipherRepromptType; + key: string; constructor(response: any) { super(response); @@ -90,5 +91,6 @@ export class CipherResponse extends BaseResponse { } this.reprompt = this.getResponseProperty("Reprompt") || CipherRepromptType.None; + this.key = this.getResponseProperty("Key") || null; } } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 4df3d202ff..2c9adce553 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,15 +1,24 @@ -// eslint-disable-next-line no-restricted-imports import { mock, mockReset } from "jest-mock-extended"; +import { of } from "rxjs"; +import { makeStaticByteArray } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { SettingsService } from "../../abstractions/settings.service"; import { UriMatchType, FieldType } from "../../enums"; +import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.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 { CipherRepromptType } from "../enums/cipher-reprompt-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 { CipherPartialRequest } from "../models/request/cipher-partial.request"; import { CipherRequest } from "../models/request/cipher.request"; +import { CipherView } from "../models/view/cipher.view"; import { CipherService } from "./cipher.service"; +const ENCRYPTED_TEXT = "This data has been encrypted"; +const ENCRYPTED_BYTES = mock(); + const cipherData: CipherData = { id: "id", organizationId: "orgId", @@ -35,6 +48,7 @@ const cipherData: CipherData = { notes: "EncryptedString", creationDate: "2022-01-01T12:00:00.000Z", deletedDate: null, + key: "EncKey", reprompt: CipherRepromptType.None, login: { uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }], @@ -88,6 +102,7 @@ describe("Cipher Service", () => { const i18nService = mock(); const searchService = mock(); const encryptService = mock(); + const configService = mock(); let cipherService: CipherService; let cipherObj: Cipher; @@ -101,6 +116,12 @@ describe("Cipher Service", () => { mockReset(i18nService); mockReset(searchService); 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( cryptoService, @@ -110,7 +131,8 @@ describe("Cipher Service", () => { searchService, stateService, encryptService, - cipherFileUploadService + cipherFileUploadService, + configService ); cipherObj = new Cipher(cipherData); @@ -125,6 +147,12 @@ describe("Cipher Service", () => { cryptoService.makeDataEncKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32))) ); + + configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(false)); + process.env.FLAGS = JSON.stringify({ + enableCipherKeyEncryption: false, + }); + const spy = jest.spyOn(cipherFileUploadService, "upload"); await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData); @@ -216,4 +244,68 @@ describe("Cipher Service", () => { 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(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(); + }); + }); + }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 03c70cca84..b9bbc3e291 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,13 +1,18 @@ +import { firstValueFrom } from "rxjs"; +import { SemVer } from "semver"; + import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { SettingsService } from "../../abstractions/settings.service"; import { FieldType, UriMatchType } from "../../enums"; import { ErrorResponse } from "../../models/response/error.response"; import { View } from "../../models/view/view"; +import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { flagEnabled } from "../../platform/misc/flags"; import { sequentialize } from "../../platform/misc/sequentialize"; import { Utils } from "../../platform/misc/utils"; 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 { PasswordHistoryView } from "../models/view/password-history.view"; +const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2023.9.1"); + export class CipherService implements CipherServiceAbstraction { private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( this.sortCiphersByLastUsed @@ -60,7 +67,8 @@ export class CipherService implements CipherServiceAbstraction { private searchService: SearchService, private stateService: StateService, private encryptService: EncryptService, - private cipherFileUploadService: CipherFileUploadService + private cipherFileUploadService: CipherFileUploadService, + private configService: ConfigServiceAbstraction ) {} async getDecryptedCipherCache(): Promise { @@ -85,63 +93,18 @@ export class CipherService implements CipherServiceAbstraction { async encrypt( model: CipherView, - key?: SymmetricCryptoKey, + keyForEncryption?: SymmetricCryptoKey, + keyForCipherKeyDecryption?: SymmetricCryptoKey, originalCipher: Cipher = null ): Promise { - // Adjust password history if (model.id != null) { if (originalCipher == null) { originalCipher = await this.get(model.id); } if (originalCipher != null) { - const existingCipher = await originalCipher.decrypt(); - 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); + await this.updateModelfromExistingCipher(model, originalCipher); } + this.adjustPasswordHistoryLength(model); } const cipher = new Cipher(); @@ -155,35 +118,32 @@ export class CipherService implements CipherServiceAbstraction { cipher.reprompt = model.reprompt; cipher.edit = model.edit; - if (key == null && cipher.organizationId != null) { - key = await this.cryptoService.getOrgKey(cipher.organizationId); - if (key == null) { - throw new Error("Cannot encrypt cipher for organization. No key."); - } - } - await Promise.all([ - this.encryptObjProperty( + if (await this.getCipherKeyEncryptionEnabled()) { + cipher.key = originalCipher?.key ?? null; + const userOrOrgKey = await this.getKeyForCipherKeyDecryption(cipher); + // 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; + // If the caller has provided a key for cipher key decryption, use it. Otherwise, use the user or org key. + keyForCipherKeyDecryption ||= userOrOrgKey; + return this.encryptCipherWithCipherKey( 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; + keyForEncryption, + keyForCipherKeyDecryption + ); + } else { + if (keyForEncryption == null && cipher.organizationId != null) { + keyForEncryption = await this.cryptoService.getOrgKey(cipher.organizationId); + if (keyForEncryption == null) { + throw new Error("Cannot encrypt cipher for organization. No key."); + } + } + // We want to ensure that the cipher key is null if cipher key encryption is disabled + // so that decryption uses the proper key. + cipher.key = null; + return this.encryptCipher(model, cipher, keyForEncryption); + } } async encryptAttachments( @@ -579,7 +539,7 @@ export class CipherService implements CipherServiceAbstraction { cipher.organizationId = organizationId; cipher.collectionIds = collectionIds; - const encCipher = await this.encrypt(cipher); + const encCipher = await this.encryptSharedCipher(cipher); const request = new CipherShareRequest(encCipher); const response = await this.apiService.putShareCipher(cipher.id, request); const data = new CipherData(response, collectionIds); @@ -597,7 +557,7 @@ export class CipherService implements CipherServiceAbstraction { cipher.organizationId = organizationId; cipher.collectionIds = collectionIds; promises.push( - this.encrypt(cipher).then((c) => { + this.encryptSharedCipher(cipher).then((c) => { encCiphers.push(c); }) ); @@ -645,14 +605,29 @@ export class CipherService implements CipherServiceAbstraction { data: Uint8Array, admin = false ): Promise { - let encKey: UserKey | OrgKey; - encKey = await this.cryptoService.getOrgKey(cipher.organizationId); - encKey ||= await this.cryptoService.getUserKeyWithLegacySupport(); + const encKey = await this.getKeyForCipherKeyDecryption(cipher); + const cipherKeyEncryptionEnabled = await this.getCipherKeyEncryptionEnabled(); - 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); - const encData = await this.encryptService.encryptToBytes(data, dataEncKey[0]); + //if cipher key encryption is disabled but the item has an individual key, + //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( cipher, @@ -963,8 +938,80 @@ export class CipherService implements CipherServiceAbstraction { await this.restore(restores); } + async getKeyForCipherKeyDecryption(cipher: Cipher): Promise { + return ( + (await this.cryptoService.getOrgKey(cipher.organizationId)) || + ((await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey) + ); + } + // 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 { + const keyForCipherKeyDecryption = await this.cryptoService.getUserKeyWithLegacySupport(); + return await this.encrypt(model, null, keyForCipherKeyDecryption); + } + + private async updateModelfromExistingCipher( + model: CipherView, + originalCipher: Cipher + ): Promise { + 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( attachmentView: AttachmentView, cipherId: string, @@ -1193,4 +1240,69 @@ export class CipherService implements CipherServiceAbstraction { private clearSortedCiphers() { this.sortedCiphersCache.clear(); } + + private async encryptCipher( + model: CipherView, + cipher: Cipher, + key: SymmetricCryptoKey + ): Promise { + 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 { + // 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 { + return ( + flagEnabled("enableCipherKeyEncryption") && + (await firstValueFrom( + this.configService.checkServerMeetsVersionRequirement$(CIPHER_KEY_ENC_MIN_SERVER_VER) + )) + ); + } } diff --git a/libs/exporter/src/vault-export/services/vault-export.service.ts b/libs/exporter/src/vault-export/services/vault-export.service.ts index 8609ce0d1c..c214646d9a 100644 --- a/libs/exporter/src/vault-export/services/vault-export.service.ts +++ b/libs/exporter/src/vault-export/services/vault-export.service.ts @@ -258,12 +258,15 @@ export class VaultExportService implements VaultExportServiceAbstraction { if (exportData.ciphers != null && exportData.ciphers.length > 0) { exportData.ciphers .filter((c) => c.deletedDate === null) - .forEach((c) => { + .forEach(async (c) => { const cipher = new Cipher(new CipherData(c)); exportPromises.push( - cipher.decrypt().then((decCipher) => { - decCiphers.push(decCipher); - }) + this.cipherService + .getKeyForCipherKeyDecryption(cipher) + .then((key) => cipher.decrypt(key)) + .then((decCipher) => { + decCiphers.push(decCipher); + }) ); }); } diff --git a/libs/importer/spec/bitwarden-password-protected-importer.spec.ts b/libs/importer/spec/bitwarden-password-protected-importer.spec.ts index 0f8f641862..5d24cef414 100644 --- a/libs/importer/spec/bitwarden-password-protected-importer.spec.ts +++ b/libs/importer/spec/bitwarden-password-protected-importer.spec.ts @@ -4,6 +4,7 @@ import { KdfType } from "@bitwarden/common/enums"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { BitwardenPasswordProtectedImporter, @@ -17,6 +18,7 @@ describe("BitwardenPasswordProtectedImporter", () => { let importer: BitwardenPasswordProtectedImporter; let cryptoService: MockProxy; let i18nService: MockProxy; + let cipherService: MockProxy; const password = Utils.newGuid(); const promptForPassword_callback = async () => { return password; @@ -25,10 +27,12 @@ describe("BitwardenPasswordProtectedImporter", () => { beforeEach(() => { cryptoService = mock(); i18nService = mock(); + cipherService = mock(); importer = new BitwardenPasswordProtectedImporter( cryptoService, i18nService, + cipherService, promptForPassword_callback ); }); diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index 5c281dc6b7..895b64e91c 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -6,6 +6,7 @@ import { import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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 { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { @@ -25,7 +26,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { protected constructor( protected cryptoService: CryptoService, - protected i18nService: I18nService + protected i18nService: I18nService, + protected cipherService: CipherService ) { 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.result.ciphers.push(view); } diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts index a8c2a711a0..49288e9dd8 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts @@ -4,21 +4,24 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; 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 { ImportResult } from "../../models/import-result"; import { Importer } from "../importer"; import { BitwardenJsonImporter } from "./bitwarden-json-importer"; + export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer { private key: SymmetricCryptoKey; constructor( cryptoService: CryptoService, i18nService: I18nService, + cipherService: CipherService, private promptForPassword_callback: () => Promise ) { - super(cryptoService, i18nService); + super(cryptoService, i18nService, cipherService); } async parse(data: string): Promise { diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 4ff15174c5..437e51436c 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -203,6 +203,7 @@ export class ImportService implements ImportServiceAbstraction { return new BitwardenPasswordProtectedImporter( this.cryptoService, this.i18nService, + this.cipherService, promptForPassword_callback ); case "lastpasscsv":