Trusted Device Encryption feature (#5950)

* PM-1049 - Create first display draft of login-decryption-options base and web components (no data loading or user actions wired up yet; WIP)

* PM-1049 - Update DeviceResponse to match latest properties on backend

* PM-1049 - Add getDevices call to retrieve all user devices to API service

* PM-1049 - WIP on figuring out login decryption options component requirements

* PM-1049 - Add empty login decryption options to desktop

* PM-1049 - Desktop - Update "Log in initiated" translation to be "Login Initiated" per figma and product request

* PM-1049 - Desktop - login decryption options component html done

* PM-1049 - Move login-decryption-options in web into own folder

* PM-1049 - Browser - created html for login-decryption-options component

* PM-1049 - Move newly created getDevices() method out of api.service into proper place in new devices-api.service.

* PM-1049 -Comment cleanup and TODO added

* PM-1049 - Comment cleanup and dependency cleanup across all login-decryption-options comps

* PM-1049 - WIP of building out needed response and regular models for saving new UserDecryptionOptions on the Account in state.

* PM-1049 - Update all User Decryption Options response and state models in light of the back end changes from a list to an object.  Web building now with decryption options stored on state under the account successfully. Must now build out state service methods for retrieving / setting account decryption options for use elsewhere.

* PM-1049 - State Service - setup setters / getters for UserDecryptionOptions off the account

* PM-1049 - StateService - replace User with Acct for decryption options

* PM-1049 - Create domain models vs using response models as response models have a response property w/ the full response nested underneath which we don't need to persist for the user decryption options stored on the account.

* PM-1049 - AcctDecryptionOptions now persist across page refreshes of the login-initiated page to act similarly to refreshes on the lock screen. Accomplished via persisting AcctDecryptionOptions in local storage -- still cleared on logout.

* PM-1049 - IdTokenResponse - only userDecryptionOptions if they exist on the response from the server; I saw a few instances where it did not. Wasn't able to replicate consistently, but I put this check here to be safe.

* PM-1049 - Login Initiated route can only be accessed if user is AuthN w/ locked vault + TDE feature flag is on.

* PM-1049 - LoginDecryptionOptions - (1) Wire up loading logic (2) Retrieve User Acct Decryption options to determine whether or not to show request admin approval btn and approve w/ MP (3) Write up future logic for requestAdminApproval (4) approveWithMasterPassword takes you to the lock screen to login.

* PM-1049 - Apply same guards as in web to login-decryption-options in desktop & browser.

* PM-1049 - (1) Updated dependencies in parent BaseLoginDecryptionOptionsComponent class + child components (2) Retrieve userEmail b/c needed for displaying which email the user is logging in with (3) Add log out functionality (4) Add comments regarding future implementation details for each login approval flow.

* PM-1049 - Web/Browser/Desktop LoginDecryptionOptions - (1) Wire up approval buttons (2) Add conditional margins (3) Loading spinner added (4) Display userEmail + "not you" logout link

* PM-1049 - Add TODOs for future changes needed as part of the Login Approval flows  for TDE

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

* add new storage to replace MasterKey with UserSymKey

* add storage for master key encrypted user symmetric key

* Begin refactor of crypto service to support new key structure

* remove provided key from getKeyForUserEncryption

* add decryption with MasterKey method to crypto service

* update makeKeyPair on crypto service to be generic

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

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

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

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

* add new memory concept to crypto service calls in cli

* update auth service to use new crypto service

* update register component in lib to use new crypto service

* update register component again with more crypto service

* update sync service to use new crypto service methods

* update send service to use new crypto service methods

* update folder service to use new crypto service methods

* update cipher service to use new crypto service

* update password generation service to use new crypto service

* update vault timeout service with new crypto service

* update collection service to use new crypto service

* update emergency access components to use new crypto service methods

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

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

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

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

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

* remove additional keys in state service clean

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

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

* migrate biometrics key
- migrate only on retrieval

* fix crypto calls for key connector and vault timeout settings

* update change password components with new crypto service

* update assortment of leftover old crypto service calls

* update device-crypto service with new crypto service

* remove old EncKey methods from crypto service

* remove clearEncKey from crypto service

* move crypto service jsdoc to abstraction

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

* fix typing of bulk confirm component

* fix EncString serialization issues & various fixes

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

* update account model with new keys serialization

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

* add early exit to native messaging flow for errors

* improve error strings in crypto service

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

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

* share disk cache to fix syncing issues between contexts

* check for ephemeral pin before process reload

* remove state no longer needed and add JSDOC

* fix linter

* add new types to tests

* remove cryptoMasterKeyB64 from account

* fix tests imports

* use master key for device approvals still

* cleanup old TODOs, add missing crypto service parameters

* fix cli crypto service calls

* share disk cache between contexts on browser

* Revert "share disk cache between contexts on browser"

This reverts commit 56a590c491.

* use user sym key for account changing unlock verification

* 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

* revert sharing disk cache between contexts

* fix tests

* add better tests to crypto service

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

* prevent duplicate cache deletes in browser

* fix browser state service tests

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* [PM-1203] fix: cli build

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

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

* Update tests to match device trust crypto service implementation changes

* update comment about state service

* update pinLockType states and add jsdocs

* add missed pinLockType changes

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

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

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

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

* [PM-1033] chore: add todo

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

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

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

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

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

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

* [PM-1033] feat: simplify approval required

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

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

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

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

* [PM-1033] feat: optimize data fetching

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

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

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

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

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

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

---------

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

* remove extra partial key

* set master key on lock component

* rename key hash to password hash on crypto service

* fix cli

* rename enc user key setter in crypto service

* Adds Events & Human Readable Messages (#5746)

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

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

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

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

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

* fix merge issues

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

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

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

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

* PM-1260 - Add new encrypted keys to TrustedDeviceUserDecryptionOptionResponse

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

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

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

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

* PM-1260 - Account - Add deviceKey to fromJSON

* TODO: add device key tests to account keys

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

* PM-1260 - Add test suite for decryptUserKeyWithDeviceKey

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

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

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

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

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

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

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

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

* PM-1260 - CLI - Update AuthSvc deps

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

* PM-1260 - Resolving more minor PR feedback

* PM-1260 - DeviceTrustCryptoService - remove debug warn

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

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

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

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

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

per PR feedback

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

---------

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

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

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

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

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

* [PM-2930] feat: remove MP copy

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

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

* Switch to retrieving approving device from token response

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

* Update Naming

* Update Test

* Update Missing Names

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

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

* fix: buffer null error (#5856)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* PM-2759 - Fix typo

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

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

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

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

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

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

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

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

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

* Add tests for enabled()

* Update state service method call

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

* rename password hash to master key hash

* fix cli build from key hash renaming

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

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

* Implement Rotation of Current Device Keys

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

* Address PR Feedback

* Move Files to Auth Ownership

* fix: getOrgKeys returning null

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

* create new reset enrollment service

* refactor: login decryption options according to TODO

* feat: add tests

* PM-3143 - Add override to overriden methods

---------

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

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

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

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

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

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

* Add service factories for user verification services

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

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

* context menu test fixes

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

* Removed buttons (#5935)

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

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

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

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

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

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

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

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

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

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

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

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

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

* [PM-1200] fix: order of dependencies

`UserVerificationService` is needed by `VaultTimeoutSettingsService`

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

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

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

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

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

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

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

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

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

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

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

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

* [PM-1200] fix: element not updating

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

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

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

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

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

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

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

This reverts commit b27f425f48570d0d5dbc9dedb9797023fef64d8b.

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

This reverts commit 766c15bc3dbadcf7dcef3053b148e7874f8939ce.

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

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

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

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

* [PM-1200] fix: cli not building

---------

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

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

* Create MasterKey from Password

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

* Add getOrDeriveKey Helper

* Use Helper In More Places

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

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

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

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

* [PM-3169] feat: close SSO window

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

* [PM-3169] fix: tests

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

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

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

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

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

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

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

* PM-3169 - Replace onSuccessfulLogin type to compile

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

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

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

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

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

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

---------

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

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

* Updated lock component to handle no master password.

* Added a comment.

* Add Missing Slash (#5967)

* Fix AdminAuthRequest Serialization on Desktop (#5970)

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

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

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

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

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

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

* Add TODO for SSO Login Strategy tests

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

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

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

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

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

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

* PM-3345 - Final tweak to logic

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

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

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

* Only check to migrate key on VaultTimeout startup

* Remove desktop specific check

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

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

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

* Add optional chaining to master key (#6007)

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

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

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

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

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

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

---------

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

* Updated email change component to getOrDeriveMasterKey (#6009)

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

* Add Listener For Events that Need To Redraw the Menu

* Send redrawMenu Message When Pin/Biometrics Updated

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

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

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

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

* TODO

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

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

* PM-3351 - remove sso comp incorrect todo

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

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

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

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

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

* Fallback to OTP When MasterPassword Hasn't Been Used

* Update Test and Rename Method

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

This reverts commit 6ec22f9570.

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

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

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

* Renamed variable for consistency.

* Updated to base logic on account.

* Fixed menubar

* Resolve merge errors in crypto service spec

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

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

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

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

* Updated warning message. (#6059)

* Tde pr feedback (#6051)

* move pin migration to the crypto service

* refactor config service logic

* refactor lock component load logic

* rename key connector methods

* add date to backwards compat todo

* update backwards compat todo

* don't specify defaults in redirectGuard

* nit

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

* fix ui tests

* add todo for tech debt

* add todo comment

* Fix storybook per PR feedback

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

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

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

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

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

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

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

---------

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

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

* TDE PR Feedback resolutions round 2 (#6068)

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

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

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

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

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

---------

Co-authored-by: Jared Snider <jsnider@bitwarden.com>
Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Jacob Fink <jfink@bitwarden.com>
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Andreas Coroiu <andreas@andreascoroiu.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: André Bispo <abispo@bitwarden.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: Jonathan Prusik <jprusik@classynemesis.com>
Co-authored-by: Matt Gibson <git@mgibson.dev>
This commit is contained in:
Todd Martin 2023-08-18 14:05:08 -04:00 committed by GitHub
parent 56b0fffdc8
commit 5665576147
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
249 changed files with 10155 additions and 2558 deletions

View File

@ -2,10 +2,7 @@
./apps/browser/src/safari/desktop/Assets.xcassets/AccentColor.colorset
./apps/browser/src/safari/desktop/Assets.xcassets/AppIcon.appiconset
./apps/browser/src/safari/desktop/Base.lproj
./apps/browser/src/services/vaultTimeout
./apps/browser/store/windows/Assets
./libs/common/src/abstractions/vaultTimeout
./libs/common/src/services/vaultTimeout
./bitwarden_license/README.md
./libs/angular/src/directives/cipherListVirtualScroll.directive.ts
./libs/angular/src/scss/webfonts/Open_Sans-italic-700.woff
@ -25,11 +22,7 @@
./libs/common/src/misc/linkedFieldOption.decorator.ts
./libs/common/src/misc/serviceUtils.ts
./libs/common/src/misc/serviceUtils.spec.ts
./libs/common/src/abstractions/vaultTimeout/vaultTimeoutSettings.service.ts
./libs/common/src/abstractions/vaultTimeout/vaultTimeout.service.ts
./libs/common/src/abstractions/anonymousHub.service.ts
./libs/common/src/services/vaultTimeout/vaultTimeoutSettings.service.ts
./libs/common/src/services/vaultTimeout/vaultTimeout.service.ts
./libs/common/src/services/anonymousHub.service.ts
./libs/auth/README.md
./README.md
@ -78,5 +71,4 @@
./apps/browser/src/safari/safari/SafariWebExtensionHandler.swift
./apps/browser/src/safari/safari/Info.plist
./apps/browser/src/safari/desktop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
./apps/browser/src/services/vaultTimeout/vaultTimeout.service.ts
./SECURITY.md

View File

@ -338,6 +338,9 @@
"other": {
"message": "Other"
},
"unlockMethodNeededToChangeTimeoutActionDesc": {
"message": "Set up an unlock method to change your vault timeout action."
},
"rateExtension": {
"message": "Rate the extension"
},
@ -1602,6 +1605,12 @@
"biometricsNotSupportedDesc": {
"message": "Browser biometrics is not supported on this device."
},
"biometricsFailedTitle": {
"message": "Biometrics failed"
},
"biometricsFailedDesc": {
"message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support."
},
"nativeMessaginPermissionErrorTitle": {
"message": "Permission not provided"
},
@ -2143,8 +2152,8 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
"logInInitiated": {
"message": "Log in initiated"
"loginInitiated": {
"message": "Login initiated"
},
"exposedMasterPassword": {
"message": "Exposed Master Password"
@ -2230,6 +2239,31 @@
"opensInANewWindow": {
"message": "Opens in a new window"
},
"deviceApprovalRequired": {
"message": "Device approval required. Select an approval option below:"
},
"rememberThisDevice": {
"message": "Remember this device"
},
"uncheckIfPublicDevice": {
"message": "Uncheck if using a public device"
},
"approveFromYourOtherDevice": {
"message": "Approve from your other device"
},
"requestAdminApproval": {
"message": "Request admin approval"
},
"approveWithMasterPassword": {
"message": "Approve with master password"
},
"ssoIdentifierRequired": {
"message": "Organization SSO identifier is required."
},
"eu": {
"message": "EU",
"description": "European Union"
},
"usDomain": {
"message": "bitwarden.com"
},
@ -2244,5 +2278,29 @@
},
"display": {
"message": "Display"
},
"accountSuccessfullyCreated": {
"message": "Account successfully created!"
},
"adminApprovalRequested": {
"message": "Admin approval requested"
},
"adminApprovalRequestSentToAdmins": {
"message": "Your request has been sent to your admin."
},
"youWillBeNotifiedOnceApproved": {
"message": "You will be notified once approved."
},
"troubleLoggingIn": {
"message": "Trouble logging in?"
},
"loginApproved": {
"message": "Login approved"
},
"userEmailMissing": {
"message": "User email missing"
},
"deviceTrusted": {
"message": "Device trusted"
}
}

View File

@ -0,0 +1,29 @@
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
} from "../../../platform/background/service-factories/crypto-service.factory";
import {
CachedServices,
FactoryOptions,
factory,
} from "../../../platform/background/service-factories/factory-options";
type AuthRequestCryptoServiceFactoryOptions = FactoryOptions;
export type AuthRequestCryptoServiceInitOptions = AuthRequestCryptoServiceFactoryOptions &
CryptoServiceInitOptions;
export function authRequestCryptoServiceFactory(
cache: { authRequestCryptoService?: AuthRequestCryptoServiceAbstraction } & CachedServices,
opts: AuthRequestCryptoServiceInitOptions
): Promise<AuthRequestCryptoServiceAbstraction> {
return factory(
cache,
"authRequestCryptoService",
opts,
async () => new AuthRequestCryptoServiceImplementation(await cryptoServiceFactory(cache, opts))
);
}

View File

@ -52,6 +52,14 @@ import {
PasswordStrengthServiceInitOptions,
} from "../../../tools/background/service_factories/password-strength-service.factory";
import {
authRequestCryptoServiceFactory,
AuthRequestCryptoServiceInitOptions,
} from "./auth-request-crypto-service.factory";
import {
deviceTrustCryptoServiceFactory,
DeviceTrustCryptoServiceInitOptions,
} from "./device-trust-crypto-service.factory";
import {
keyConnectorServiceFactory,
KeyConnectorServiceInitOptions,
@ -75,7 +83,9 @@ export type AuthServiceInitOptions = AuthServiceFactoyOptions &
I18nServiceInitOptions &
EncryptServiceInitOptions &
PolicyServiceInitOptions &
PasswordStrengthServiceInitOptions;
PasswordStrengthServiceInitOptions &
DeviceTrustCryptoServiceInitOptions &
AuthRequestCryptoServiceInitOptions;
export function authServiceFactory(
cache: { authService?: AbstractAuthService } & CachedServices,
@ -101,7 +111,9 @@ export function authServiceFactory(
await i18nServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
await passwordStrengthServiceFactory(cache, opts),
await policyServiceFactory(cache, opts)
await policyServiceFactory(cache, opts),
await deviceTrustCryptoServiceFactory(cache, opts),
await authRequestCryptoServiceFactory(cache, opts)
)
);
}

View File

@ -0,0 +1,74 @@
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import {
DevicesApiServiceInitOptions,
devicesApiServiceFactory,
} from "../../../background/service-factories/devices-api-service.factory";
import {
AppIdServiceInitOptions,
appIdServiceFactory,
} from "../../../platform/background/service-factories/app-id-service.factory";
import {
CryptoFunctionServiceInitOptions,
cryptoFunctionServiceFactory,
} from "../../../platform/background/service-factories/crypto-function-service.factory";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
} from "../../../platform/background/service-factories/crypto-service.factory";
import {
EncryptServiceInitOptions,
encryptServiceFactory,
} from "../../../platform/background/service-factories/encrypt-service.factory";
import {
CachedServices,
FactoryOptions,
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
I18nServiceInitOptions,
i18nServiceFactory,
} from "../../../platform/background/service-factories/i18n-service.factory";
import {
PlatformUtilsServiceInitOptions,
platformUtilsServiceFactory,
} from "../../../platform/background/service-factories/platform-utils-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory";
type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions;
export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions &
CryptoFunctionServiceInitOptions &
CryptoServiceInitOptions &
EncryptServiceInitOptions &
StateServiceInitOptions &
AppIdServiceInitOptions &
DevicesApiServiceInitOptions &
I18nServiceInitOptions &
PlatformUtilsServiceInitOptions;
export function deviceTrustCryptoServiceFactory(
cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices,
opts: DeviceTrustCryptoServiceInitOptions
): Promise<DeviceTrustCryptoServiceAbstraction> {
return factory(
cache,
"deviceTrustCryptoService",
opts,
async () =>
new DeviceTrustCryptoService(
await cryptoFunctionServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await appIdServiceFactory(cache, opts),
await devicesApiServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts)
)
);
}

View File

@ -0,0 +1,29 @@
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
import {
ApiServiceInitOptions,
apiServiceFactory,
} from "../../../platform/background/service-factories/api-service.factory";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
type UserVerificationApiServiceFactoryOptions = FactoryOptions;
export type UserVerificationApiServiceInitOptions = UserVerificationApiServiceFactoryOptions &
ApiServiceInitOptions;
export function userVerificationApiServiceFactory(
cache: { userVerificationApiService?: UserVerificationApiServiceAbstraction } & CachedServices,
opts: UserVerificationApiServiceInitOptions
): Promise<UserVerificationApiServiceAbstraction> {
return factory(
cache,
"userVerificationApiService",
opts,
async () => new UserVerificationApiService(await apiServiceFactory(cache, opts))
);
}

View File

@ -0,0 +1,51 @@
import { UserVerificationService as AbstractUserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
} from "../../../platform/background/service-factories/crypto-service.factory";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
I18nServiceInitOptions,
i18nServiceFactory,
} from "../../../platform/background/service-factories/i18n-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory";
import {
UserVerificationApiServiceInitOptions,
userVerificationApiServiceFactory,
} from "./user-verification-api-service.factory";
type UserVerificationServiceFactoryOptions = FactoryOptions;
export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryOptions &
StateServiceInitOptions &
CryptoServiceInitOptions &
I18nServiceInitOptions &
UserVerificationApiServiceInitOptions;
export function userVerificationServiceFactory(
cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices,
opts: UserVerificationServiceInitOptions
): Promise<AbstractUserVerificationService> {
return factory(
cache,
"userVerificationService",
opts,
async () =>
new UserVerificationService(
await stateServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
await userVerificationApiServiceFactory(cache, opts)
)
);
}

View File

@ -5,14 +5,20 @@
<span class="title">{{ "verifyIdentity" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" *ngIf="!hideInput">{{ "unlock" | i18n }}</button>
<button type="submit" *ngIf="pinEnabled || masterPasswordEnabled">
{{ "unlock" | i18n }}
</button>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow *ngIf="!hideInput">
<div class="row-main" *ngIf="pinLock">
<div
class="box-content-row box-content-row-flex"
appBoxRow
*ngIf="pinEnabled || masterPasswordEnabled"
>
<div class="row-main" *ngIf="pinEnabled">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
@ -24,7 +30,7 @@
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="!pinLock">
<div class="row-main" *ngIf="masterPasswordEnabled && !pinEnabled">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"

View File

@ -3,12 +3,13 @@ import { Router } from "@angular/router";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -44,13 +45,14 @@ export class LockComponent extends BaseLockComponent {
stateService: StateService,
apiService: ApiService,
logService: LogService,
keyConnectorService: KeyConnectorService,
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordStrengthService: PasswordStrengthServiceAbstraction,
private authService: AuthService,
dialogService: DialogService
dialogService: DialogService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
userVerificationService: UserVerificationService
) {
super(
router,
@ -64,12 +66,13 @@ export class LockComponent extends BaseLockComponent {
stateService,
apiService,
logService,
keyConnectorService,
ngZone,
policyApiService,
policyService,
passwordStrengthService,
dialogService
dialogService,
deviceTrustCryptoService,
userVerificationService
);
this.successRoute = "/tabs/current";
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
@ -81,7 +84,7 @@ export class LockComponent extends BaseLockComponent {
(await this.stateService.getDisableAutoBiometricsPrompt()) ?? true;
window.setTimeout(async () => {
document.getElementById(this.pinLock ? "pin" : "masterPassword").focus();
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
if (
this.biometricLock &&
!disableAutoBiometricsPrompt &&
@ -93,7 +96,7 @@ export class LockComponent extends BaseLockComponent {
}, 100);
}
async unlockBiometric(): Promise<boolean> {
override async unlockBiometric(): Promise<boolean> {
if (!this.biometricLock) {
return;
}

View File

@ -0,0 +1,108 @@
<div id="login-initiated">
<header>
<h1 class="margin-auto">
<span class="title">{{ "loginInitiated" | i18n }}</span>
</h1>
</header>
<div class="content login-page">
<div class="full-loading-spinner" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<ng-container *ngIf="!loading">
<ng-container *ngIf="data.state == State.ExistingUserUntrustedDevice">
<div class="standard-x-margin">
<p class="lead">{{ "loginInitiated" | i18n }}</p>
<h6 class="mb-20px">{{ "deviceApprovalRequired" | i18n }}</h6>
</div>
<form
id="rememberDeviceForm"
class="mb-20px standard-x-margin"
[formGroup]="rememberDeviceForm"
>
<div>
<input
type="checkbox"
id="rememberDevice"
name="rememberDevice"
formControlName="rememberDevice"
/>
<label for="rememberDevice">
{{ "rememberThisDevice" | i18n }}
</label>
<p id="rememberThisDeviceHintText">{{ "uncheckIfPublicDevice" | i18n }}</p>
</div>
</form>
<div class="box mb-20px">
<button
*ngIf="data.showApproveFromOtherDeviceBtn"
(click)="approveFromOtherDevice()"
type="button"
class="btn primary block"
>
<b>{{ "approveFromYourOtherDevice" | i18n }}</b>
</button>
<button
*ngIf="data.showReqAdminApprovalBtn"
(click)="requestAdminApproval()"
type="button"
class="btn block btn-top-margin"
>
{{ "requestAdminApproval" | i18n }}
</button>
<button
*ngIf="data.showApproveWithMasterPasswordBtn"
type="button"
class="btn block btn-top-margin"
(click)="approveWithMasterPassword()"
>
{{ "approveWithMasterPassword" | i18n }}
</button>
</div>
</ng-container>
<ng-container *ngIf="data.state == State.NewUser">
<div class="standard-x-margin">
<p class="lead">{{ "loginInitiated" | i18n }}</p>
</div>
<form
id="rememberDeviceForm"
class="mb-20px standard-x-margin"
[formGroup]="rememberDeviceForm"
>
<div>
<input
type="checkbox"
id="rememberDevice"
name="rememberDevice"
formControlName="rememberDevice"
/>
<label for="rememberDevice">
{{ "rememberThisDevice" | i18n }}
</label>
<p id="rememberThisDeviceHintText">{{ "uncheckIfPublicDevice" | i18n }}</p>
</div>
</form>
<div class="box mb-20px">
<button (click)="createUser()" type="button" class="btn primary block">
<b>{{ "continue" | i18n }}</b>
</button>
</div>
</ng-container>
<hr class="muted-hr mx-5px mb-20px" />
<div class="small mx-5px">
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ data.userEmail }}</p>
<a tabindex="0" role="button" style="cursor: pointer" (click)="logOut()">{{
"notYou" | i18n
}}</a>
</div>
</ng-container>
</div>
</div>

View File

@ -0,0 +1,18 @@
import { Component } from "@angular/core";
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
@Component({
selector: "browser-login-decryption-options",
templateUrl: "login-decryption-options.component.html",
})
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
override async createUser(): Promise<void> {
try {
await super.createUser();
await this.router.navigate(["/tabs/vault"]);
} catch (error) {
this.validationService.showError(error);
}
}
}

View File

@ -5,32 +5,57 @@
</h1>
</header>
<div class="content login-page">
<div>
<p class="lead">{{ "logInInitiated" | i18n }}</p>
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<div>
<p>{{ "notificationSentDevice" | i18n }}</p>
<p class="lead">{{ "loginInitiated" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
</p>
<div>
<p>{{ "notificationSentDevice" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<div>
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
<p class="fingerprint-text">
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<div class="resend-notification" *ngIf="showResendNotification">
<a (click)="startPasswordlessLogin()">{{ "resendNotification" | i18n }}</a>
</div>
<div class="footer">
{{ "loginWithDeviceEnabledInfo" | i18n }}
<a href="#" (click)="back()">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
</ng-container>
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
<div>
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
<p class="fingerprint-text">
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<p class="lead">{{ "adminApprovalRequested" | i18n }}</p>
<div class="resend-notification" *ngIf="showResendNotification">
<a (click)="startPasswordlessLogin()">{{ "resendNotification" | i18n }}</a>
</div>
<div>
<p>{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
<p>{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
</div>
<div class="footer">
{{ "loginWithDeviceEnabledInfo" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
<div>
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
<p class="fingerprint-text">
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<div class="footer">
{{ "troubleLoggingIn" | i18n }}
<a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
</div>
</ng-container>
</div>
</div>

View File

@ -1,10 +1,13 @@
import { Location } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-with-device.component";
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -42,7 +45,10 @@ export class LoginWithDeviceComponent
validationService: ValidationService,
stateService: StateService,
loginService: LoginService,
syncService: SyncService
syncService: SyncService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
authReqCryptoService: AuthRequestCryptoServiceAbstraction,
private location: Location
) {
super(
router,
@ -59,10 +65,16 @@ export class LoginWithDeviceComponent
anonymousHubService,
validationService,
stateService,
loginService
loginService,
deviceTrustCryptoService,
authReqCryptoService
);
super.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
};
}
protected back() {
this.location.back();
}
}

View File

@ -4,8 +4,8 @@ import { ActivatedRoute, Router } from "@angular/router";
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";

View File

@ -1,2 +1 @@
export { LockGuardService } from "./lock-guard.service";
export { UnauthGuardService } from "./unauth-guard.service";

View File

@ -1,8 +0,0 @@
import { Injectable } from "@angular/core";
import { LockGuard as BaseLockGuardService } from "@bitwarden/angular/auth/guards/lock.guard";
@Injectable()
export class LockGuardService extends BaseLockGuardService {
protected homepage = "tabs/current";
}

View File

@ -1,6 +1,6 @@
import { Injectable } from "@angular/core";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards/unauth.guard";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
@Injectable()
export class UnauthGuardService extends BaseUnauthGuardService {

View File

@ -1,11 +1,12 @@
import { Component } from "@angular/core";
import { Component, Inject } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -35,7 +36,8 @@ export class SsoComponent extends BaseSsoComponent {
syncService: SyncService,
environmentService: EnvironmentService,
logService: LogService,
private vaultTimeoutService: VaultTimeoutService
configService: ConfigServiceAbstraction,
@Inject(WINDOW) private win: Window
) {
super(
authService,
@ -48,7 +50,8 @@ export class SsoComponent extends BaseSsoComponent {
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
logService,
configService
);
const url = this.environmentService.getWebVaultUrl();
@ -57,15 +60,22 @@ export class SsoComponent extends BaseSsoComponent {
this.clientId = "browser";
super.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
syncService.fullSync(true);
// If the vault is unlocked then this will clear keys from memory, which we don't want to do
if ((await this.authService.getAuthStatus()) !== AuthenticationStatus.Unlocked) {
BrowserApi.reloadOpenWindows();
}
const thisWindow = window.open("", "_self");
thisWindow.close();
this.win.close();
};
super.onSuccessfulLoginTde = async () => {
syncService.fullSync(true);
};
super.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
}
}

View File

@ -1,8 +1,9 @@
import { Component } from "@angular/core";
import { Component, Inject } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
@ -10,6 +11,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -48,7 +50,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService,
private dialogService: DialogService
configService: ConfigServiceAbstraction,
private dialogService: DialogService,
@Inject(WINDOW) protected win: Window
) {
super(
authService,
@ -56,19 +60,28 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
i18nService,
apiService,
platformUtilsService,
window,
win,
environmentService,
stateService,
route,
logService,
twoFactorService,
appIdService,
loginService
loginService,
configService
);
super.onSuccessfulLogin = () => {
this.loginService.clearValues();
return syncService.fullSync(true);
super.onSuccessfulLogin = async () => {
syncService.fullSync(true);
};
super.onSuccessfulLoginTde = async () => {
syncService.fullSync(true);
};
super.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
super.successRoute = "/tabs/vault";
// FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe
this.webAuthnNewTab = true;
@ -117,11 +130,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.sso === "true") {
super.onSuccessfulLogin = () => {
super.onSuccessfulLogin = async () => {
// This is not awaited so we don't pause the application while the sync is happening.
// This call is executed by the service that lives in the background script so it will continue
// the sync even if this tab closes.
const syncPromise = this.syncService.fullSync(true);
this.syncService.fullSync(true);
// Force sidebars (FF && Opera) to reload while exempting current window
// because we are just going to close the current window.
@ -130,8 +143,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
// We don't need this window anymore because the intent is for the user to be left
// on the web vault screen which tells them to continue in the browser extension (sidebar or popup)
BrowserApi.closeBitwardenExtensionTab();
return syncPromise;
};
}
});

View File

@ -2,6 +2,10 @@ import {
TotpServiceInitOptions,
totpServiceFactory,
} from "../../../auth/background/service-factories/totp-service.factory";
import {
UserVerificationServiceInitOptions,
userVerificationServiceFactory,
} from "../../../auth/background/service-factories/user-verification-service.factory";
import {
EventCollectionServiceInitOptions,
eventCollectionServiceFactory,
@ -38,7 +42,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
TotpServiceInitOptions &
EventCollectionServiceInitOptions &
LogServiceInitOptions &
SettingsServiceInitOptions;
SettingsServiceInitOptions &
UserVerificationServiceInitOptions;
export function autofillServiceFactory(
cache: { autofillService?: AbstractAutoFillService } & CachedServices,
@ -55,7 +60,8 @@ export function autofillServiceFactory(
await totpServiceFactory(cache, opts),
await eventCollectionServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await settingsServiceFactory(cache, opts)
await settingsServiceFactory(cache, opts),
await userVerificationServiceFactory(cache, opts)
)
);
}

View File

@ -1,6 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@ -13,6 +14,7 @@ describe("CipherContextMenuHandler", () => {
let mainContextMenuHandler: MockProxy<MainContextMenuHandler>;
let authService: MockProxy<AuthService>;
let cipherService: MockProxy<CipherService>;
let userVerificationService: MockProxy<UserVerificationService>;
let sut: CipherContextMenuHandler;
@ -20,10 +22,17 @@ describe("CipherContextMenuHandler", () => {
mainContextMenuHandler = mock();
authService = mock();
cipherService = mock();
userVerificationService = mock();
userVerificationService.hasMasterPassword.mockResolvedValue(true);
jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue();
sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService);
sut = new CipherContextMenuHandler(
mainContextMenuHandler,
authService,
cipherService,
userVerificationService
);
});
afterEach(() => jest.resetAllMocks());
@ -83,11 +92,11 @@ describe("CipherContextMenuHandler", () => {
};
cipherService.getAllDecryptedForUrl.mockResolvedValue([
null,
undefined,
{ type: CipherType.Card },
{ type: CipherType.Login, reprompt: CipherRepromptType.Password },
realCipher,
null, // invalid cipher
undefined, // invalid cipher
{ type: CipherType.Card }, // invalid cipher
{ type: CipherType.Login, reprompt: CipherRepromptType.Password }, // invalid cipher
realCipher, // valid cipher
] as any[]);
await sut.update("https://test.com");
@ -105,5 +114,61 @@ describe("CipherContextMenuHandler", () => {
realCipher
);
});
it("adds ciphers with master password reprompt if the user does not have a master password", async () => {
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
// User does not have a master password, or has one but hasn't logged in with it (key connector user or TDE user)
userVerificationService.hasMasterPasswordAndMasterKeyHash.mockResolvedValue(false);
mainContextMenuHandler.init.mockResolvedValue(true);
const realCipher = {
id: "5",
type: CipherType.Login,
reprompt: CipherRepromptType.None,
name: "Test Cipher",
login: { username: "Test Username" },
};
const repromptCipher = {
id: "6",
type: CipherType.Login,
reprompt: CipherRepromptType.Password,
name: "Test Reprompt Cipher",
login: { username: "Test Username" },
};
cipherService.getAllDecryptedForUrl.mockResolvedValue([
null, // invalid cipher
undefined, // invalid cipher
{ type: CipherType.Card }, // invalid cipher
repromptCipher, // valid cipher
realCipher, // valid cipher
] as any[]);
await sut.update("https://test.com");
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com");
// Should call this twice, once for each valid cipher
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(2);
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
"Test Cipher (Test Username)",
"5",
"https://test.com",
realCipher
);
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledWith(
"Test Reprompt Cipher (Test Username)",
"6",
"https://test.com",
repromptCipher
);
});
});
});

View File

@ -1,4 +1,5 @@
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -11,6 +12,7 @@ import {
authServiceFactory,
AuthServiceInitOptions,
} from "../../auth/background/service-factories/auth-service.factory";
import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory";
import { Account } from "../../models/account";
import { CachedServices } from "../../platform/background/service-factories/factory-options";
import { BrowserApi } from "../../platform/browser/browser-api";
@ -37,7 +39,8 @@ export class CipherContextMenuHandler {
constructor(
private mainContextMenuHandler: MainContextMenuHandler,
private authService: AuthService,
private cipherService: CipherService
private cipherService: CipherService,
private userVerificationService: UserVerificationService
) {}
static async create(cachedServices: CachedServices) {
@ -76,7 +79,8 @@ export class CipherContextMenuHandler {
return new CipherContextMenuHandler(
await MainContextMenuHandler.mv3Create(cachedServices),
await authServiceFactory(cachedServices, serviceOptions),
await cipherServiceFactory(cachedServices, serviceOptions)
await cipherServiceFactory(cachedServices, serviceOptions),
await userVerificationServiceFactory(cachedServices, serviceOptions)
);
}
@ -176,7 +180,11 @@ export class CipherContextMenuHandler {
}
private async updateForCipher(url: string, cipher: CipherView) {
if (cipher == null || cipher.type !== CipherType.Login) {
if (
cipher == null ||
cipher.type !== CipherType.Login ||
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
) {
return;
}

View File

@ -1,6 +1,7 @@
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EventType, FieldType, UriMatchType } from "@bitwarden/common/enums";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -45,7 +46,8 @@ export default class AutofillService implements AutofillServiceInterface {
private totpService: TotpService,
private eventCollectionService: EventCollectionService,
private logService: LogService,
private settingsService: SettingsService
private settingsService: SettingsService,
private userVerificationService: UserVerificationService
) {}
getFormsWithPasswordFields(pageDetails: AutofillPageDetails): FormData[] {
@ -238,7 +240,10 @@ export default class AutofillService implements AutofillServiceInterface {
return null;
}
if (cipher.reprompt !== CipherRepromptType.None) {
if (
cipher.reprompt !== CipherRepromptType.None &&
(await this.userVerificationService.hasMasterPasswordAndMasterKeyHash())
) {
await BrowserApi.tabSendMessageData(tab, "passwordReprompt", {
cipherId: cipher.id,
action: "autofill",

View File

@ -1,4 +1,4 @@
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";

View File

@ -1,5 +1,5 @@
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";

View File

@ -1,27 +1,33 @@
import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
@ -59,12 +65,13 @@ import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/we
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
import { DevicesServiceImplementation } from "@bitwarden/common/services/devices/devices.service.implementation";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { TotpService } from "@bitwarden/common/services/totp.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import {
PasswordGenerationService,
PasswordGenerationServiceAbstraction,
@ -128,7 +135,7 @@ import { KeyGenerationService } from "../platform/services/key-generation.servic
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
import { BrowserSendService } from "../services/browser-send.service";
import { BrowserSettingsService } from "../services/browser-settings.service";
import VaultTimeoutService from "../services/vaultTimeout/vaultTimeout.service";
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
import { BrowserFolderService } from "../vault/services/browser-folder.service";
import { VaultFilterService } from "../vault/services/vault-filter.service";
@ -156,7 +163,7 @@ export default class MainBackground {
cipherService: CipherServiceAbstraction;
folderService: InternalFolderServiceAbstraction;
collectionService: CollectionServiceAbstraction;
vaultTimeoutService: VaultTimeoutServiceAbstraction;
vaultTimeoutService: VaultTimeoutService;
vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
syncService: SyncServiceAbstraction;
passwordGenerationService: PasswordGenerationServiceAbstraction;
@ -196,6 +203,10 @@ export default class MainBackground {
cipherContextMenuHandler: CipherContextMenuHandler;
configService: ConfigServiceAbstraction;
configApiService: ConfigApiServiceAbstraction;
devicesApiService: DevicesApiServiceAbstraction;
devicesService: DevicesServiceAbstraction;
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
browserPopoutWindowService: BrowserPopoutWindowService;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
@ -387,6 +398,23 @@ export default class MainBackground {
that.runtimeBackground.processMessage(message, that as any, null);
};
})();
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
this.deviceTrustCryptoService = new DeviceTrustCryptoService(
this.cryptoFunctionService,
this.cryptoService,
this.encryptService,
this.stateService,
this.appIdService,
this.devicesApiService,
this.i18nService,
this.platformUtilsService
);
this.devicesService = new DevicesServiceImplementation(this.devicesApiService);
this.authRequestCryptoService = new AuthRequestCryptoServiceImplementation(this.cryptoService);
this.authService = new AuthService(
this.cryptoService,
this.apiService,
@ -402,14 +430,26 @@ export default class MainBackground {
this.i18nService,
this.encryptService,
this.passwordStrengthService,
this.policyService
this.policyService,
this.deviceTrustCryptoService,
this.authRequestCryptoService
);
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
this.userVerificationService = new UserVerificationService(
this.stateService,
this.cryptoService,
this.i18nService,
this.userVerificationApiService
);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.cryptoService,
this.tokenService,
this.policyService,
this.stateService
this.stateService,
this.userVerificationService
);
this.vaultTimeoutService = new VaultTimeoutService(
@ -420,7 +460,6 @@ export default class MainBackground {
this.platformUtilsService,
this.messagingService,
this.searchService,
this.keyConnectorService,
this.stateService,
this.authService,
this.vaultTimeoutSettingsService,
@ -471,13 +510,15 @@ export default class MainBackground {
this.eventUploadService
);
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
this.autofillService = new AutofillService(
this.cipherService,
this.stateService,
this.totpService,
this.eventCollectionService,
this.logService,
this.settingsService
this.settingsService,
this.userVerificationService
);
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
this.exportService = new VaultExportService(
@ -499,15 +540,6 @@ export default class MainBackground {
this.authService,
this.messagingService
);
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
this.userVerificationService = new UserVerificationService(
this.cryptoService,
this.i18nService,
this.userVerificationApiService
);
this.configService = new ConfigService(
this.stateService,
this.configApiService,
@ -638,7 +670,8 @@ export default class MainBackground {
this.cipherContextMenuHandler = new CipherContextMenuHandler(
this.mainContextMenuHandler,
this.authService,
this.cipherService
this.cipherService,
this.userVerificationService
);
}
}
@ -648,7 +681,7 @@ export default class MainBackground {
await this.stateService.init();
await (this.vaultTimeoutService as VaultTimeoutService).init(true);
await this.vaultTimeoutService.init(true);
await (this.i18nService as BrowserI18nService).init();
await (this.eventUploadService as EventUploadService).init(true);
await this.runtimeBackground.init();

View File

@ -10,7 +10,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BrowserApi } from "../platform/browser/browser-api";
@ -42,6 +46,7 @@ type ReceiveMessage = {
// Unlock key
keyB64?: string;
userKeyB64?: string;
};
type ReceiveMessageOuter = {
@ -320,16 +325,55 @@ export class NativeMessagingBackground {
}
if (message.response === "unlocked") {
await this.cryptoService.setKey(
new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64))
);
try {
if (message.userKeyB64) {
const userKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(message.userKeyB64)
) as UserKey;
await this.cryptoService.setUserKey(userKey);
} else if (message.keyB64) {
// Backwards compatibility to support cases in which the user hasn't updated their desktop app
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472)
let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey();
encUserKey ||= await this.stateService.getMasterKeyEncryptedUserKey();
if (!encUserKey) {
throw new Error("No encrypted user key found");
}
const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(message.keyB64)
) as MasterKey;
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(
masterKey,
new EncString(encUserKey)
);
await this.cryptoService.setMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
} else {
throw new Error("No key received");
}
} catch (e) {
this.logService.error("Unable to set key: " + e);
this.messagingService.send("showDialog", {
title: { key: "biometricsFailedTitle" },
content: { key: "biometricsFailedDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
// Exit early
if (this.resolver) {
this.resolver(message);
}
return;
}
// Verify key is correct by attempting to decrypt a secret
try {
await this.cryptoService.getFingerprint(await this.stateService.getUserId());
} catch (e) {
this.logService.error("Unable to verify key: " + e);
await this.cryptoService.clearKey();
await this.cryptoService.clearKeys();
this.showWrongUserDialog();
// Exit early

View File

@ -0,0 +1,28 @@
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import {
ApiServiceInitOptions,
apiServiceFactory,
} from "../../platform/background/service-factories/api-service.factory";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../platform/background/service-factories/factory-options";
type DevicesApiServiceFactoryOptions = FactoryOptions;
export type DevicesApiServiceInitOptions = DevicesApiServiceFactoryOptions & ApiServiceInitOptions;
export function devicesApiServiceFactory(
cache: { devicesApiService?: DevicesApiServiceAbstraction } & CachedServices,
opts: DevicesApiServiceInitOptions
): Promise<DevicesApiServiceAbstraction> {
return factory(
cache,
"devicesApiService",
opts,
async () => new DevicesApiServiceImplementation(await apiServiceFactory(cache, opts))
);
}

View File

@ -1,13 +1,9 @@
import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import {
authServiceFactory,
AuthServiceInitOptions,
} from "../../auth/background/service-factories/auth-service.factory";
import {
keyConnectorServiceFactory,
KeyConnectorServiceInitOptions,
} from "../../auth/background/service-factories/key-connector-service.factory";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
@ -29,7 +25,7 @@ import {
StateServiceInitOptions,
stateServiceFactory,
} from "../../platform/background/service-factories/state-service.factory";
import VaultTimeoutService from "../../services/vaultTimeout/vaultTimeout.service";
import VaultTimeoutService from "../../services/vault-timeout/vault-timeout.service";
import {
cipherServiceFactory,
CipherServiceInitOptions,
@ -64,7 +60,6 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
PlatformUtilsServiceInitOptions &
MessagingServiceInitOptions &
SearchServiceInitOptions &
KeyConnectorServiceInitOptions &
StateServiceInitOptions &
AuthServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions;
@ -86,7 +81,6 @@ export function vaultTimeoutServiceFactory(
await platformUtilsServiceFactory(cache, opts),
await messagingServiceFactory(cache, opts),
await searchServiceFactory(cache, opts),
await keyConnectorServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await authServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),

View File

@ -1,5 +1,5 @@
import { VaultTimeoutSettingsService as AbstractVaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService as AbstractVaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import {
policyServiceFactory,
@ -9,6 +9,10 @@ import {
tokenServiceFactory,
TokenServiceInitOptions,
} from "../../auth/background/service-factories/token-service.factory";
import {
userVerificationServiceFactory,
UserVerificationServiceInitOptions,
} from "../../auth/background/service-factories/user-verification-service.factory";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
@ -29,7 +33,8 @@ export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsService
CryptoServiceInitOptions &
TokenServiceInitOptions &
PolicyServiceInitOptions &
StateServiceInitOptions;
StateServiceInitOptions &
UserVerificationServiceInitOptions;
export function vaultTimeoutSettingsServiceFactory(
cache: { vaultTimeoutSettingsService?: AbstractVaultTimeoutSettingsService } & CachedServices,
@ -44,7 +49,8 @@ export function vaultTimeoutSettingsServiceFactory(
await cryptoServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts),
await policyServiceFactory(cache, opts),
await stateServiceFactory(cache, opts)
await stateServiceFactory(cache, opts),
await userVerificationServiceFactory(cache, opts)
)
);
}

View File

@ -1,13 +1,32 @@
import { KeySuffixOptions } from "@bitwarden/common/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
export class BrowserCryptoService extends CryptoService {
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions) {
if (keySuffix === "biometric") {
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
if (keySuffix === KeySuffixOptions.Biometric) {
return await this.stateService.getBiometricUnlock({ userId: userId });
}
return super.hasUserKeyStored(keySuffix, userId);
}
/**
* Browser doesn't store biometric keys, so we retrieve them from the desktop and return
* if we successfully saved it into memory as the User Key
*/
protected override async getKeyFromStorage(keySuffix: KeySuffixOptions): Promise<UserKey> {
if (keySuffix === KeySuffixOptions.Biometric) {
await this.platformUtilService.authenticateBiometric();
return (await this.getKey())?.keyB64;
const userKey = await this.stateService.getUserKey();
if (userKey) {
return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey.keyB64)) as UserKey;
}
}
return await super.retrieveKeyFromStorage(keySuffix);
return await super.getKeyFromStorage(keySuffix);
}
}

View File

@ -41,7 +41,8 @@ describe("Browser State Service", () => {
logService = mock();
stateMigrationService = mock();
stateFactory = mock();
useAccountCache = true;
// turn off account cache for tests
useAccountCache = false;
state = new State(new GlobalState());
state.accounts[userId] = new Account({

View File

@ -1,5 +1,12 @@
import { BehaviorSubject } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateMigrationService } from "@bitwarden/common/platform/abstractions/state-migration.service";
import {
AbstractStorageService,
AbstractMemoryStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
@ -26,14 +33,44 @@ export class BrowserStateService
protected activeAccountSubject: BehaviorSubject<string>;
@sessionSync({ initializer: (b: boolean) => b })
protected activeAccountUnlockedSubject: BehaviorSubject<boolean>;
@sessionSync({
initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account
initializeAs: "record",
})
protected accountDiskCache: BehaviorSubject<Record<string, Account>>;
protected accountDeserializer = Account.fromJSON;
constructor(
storageService: AbstractStorageService,
secureStorageService: AbstractStorageService,
memoryStorageService: AbstractMemoryStorageService,
logService: LogService,
stateMigrationService: StateMigrationService,
stateFactory: StateFactory<GlobalState, Account>,
useAccountCache = true
) {
super(
storageService,
secureStorageService,
memoryStorageService,
logService,
stateMigrationService,
stateFactory,
useAccountCache
);
// TODO: This is a hack to fix having a disk cache on both the popup and
// the background page that can get out of sync. We need to work out the
// best way to handle caching with multiple instances of the state service.
if (useAccountCache) {
chrome.storage.onChanged.addListener((changes, namespace) => {
if (namespace === "local") {
for (const key of Object.keys(changes)) {
if (key !== "accountActivity" && this.accountDiskCache.value[key]) {
this.deleteDiskCache(key);
}
}
}
});
}
}
async addAccount(account: Account) {
// Apply browser overrides to default account values
account = new Account(account);
@ -132,4 +169,17 @@ export class BrowserStateService
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
// Overriding the base class to prevent deleting the cache on save. We register a storage listener
// to delete the cache in the constructor above.
protected override async saveAccountToDisk(
account: Account,
options: StorageOptions
): Promise<void> {
const storageLocation = options.useSecureStorage
? this.secureStorageService
: this.storageService;
await storageLocation.save(`${options.userId}`, account, options);
}
}

View File

@ -1,14 +1,21 @@
import { Injectable, NgModule } from "@angular/core";
import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards/auth.guard";
import { LockGuard } from "@bitwarden/angular/auth/guards/lock.guard";
import { UnauthGuard } from "@bitwarden/angular/auth/guards/unauth.guard";
import {
redirectGuard,
AuthGuard,
lockGuard,
tdeDecryptionRequiredGuard,
UnauthGuard,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EnvironmentComponent } from "../auth/popup/environment.component";
import { HintComponent } from "../auth/popup/hint.component";
import { HomeComponent } from "../auth/popup/home.component";
import { LockComponent } from "../auth/popup/lock.component";
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
import { LoginWithDeviceComponent } from "../auth/popup/login-with-device.component";
import { LoginComponent } from "../auth/popup/login.component";
import { RegisterComponent } from "../auth/popup/register.component";
@ -49,8 +56,9 @@ import { TabsComponent } from "./tabs.component";
const routes: Routes = [
{
path: "",
redirectTo: "home",
pathMatch: "full",
children: [], // Children lets us have an empty component.
canActivate: [redirectGuard({ loggedIn: "/tabs/vault", loggedOut: "/home", locked: "/lock" })],
},
{
path: "vault",
@ -72,13 +80,19 @@ const routes: Routes = [
{
path: "login-with-device",
component: LoginWithDeviceComponent,
canActivate: [UnauthGuard],
canActivate: [],
data: { state: "login-with-device" },
},
{
path: "admin-approval-requested",
component: LoginWithDeviceComponent,
canActivate: [],
data: { state: "login-with-device" },
},
{
path: "lock",
component: LockComponent,
canActivate: [LockGuard],
canActivate: [lockGuard()],
data: { state: "lock" },
},
{
@ -93,6 +107,14 @@ const routes: Routes = [
canActivate: [UnauthGuard],
data: { state: "2fa-options" },
},
{
path: "login-initiated",
component: LoginDecryptionOptionsComponent,
canActivate: [
tdeDecryptionRequiredGuard(),
canAccessFeature(FeatureFlag.TrustedDeviceEncryption),
],
},
{
path: "sso",
component: SsoComponent,

View File

@ -20,6 +20,7 @@ import { EnvironmentComponent } from "../auth/popup/environment.component";
import { HintComponent } from "../auth/popup/hint.component";
import { HomeComponent } from "../auth/popup/home.component";
import { LockComponent } from "../auth/popup/lock.component";
import { LoginDecryptionOptionsComponent } from "../auth/popup/login-decryption-options/login-decryption-options.component";
import { LoginWithDeviceComponent } from "../auth/popup/login-with-device.component";
import { LoginComponent } from "../auth/popup/login.component";
import { RegisterComponent } from "../auth/popup/register.component";
@ -121,6 +122,7 @@ import "../platform/popup/locales";
LockComponent,
LoginComponent,
LoginWithDeviceComponent,
LoginDecryptionOptionsComponent,
OptionsComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,

View File

@ -1,4 +1,4 @@
<ng-container *ngIf="!usesKeyConnector">
<ng-container *ngIf="hasMasterPassword">
<div class="box-content-row" appBoxRow>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
@ -14,7 +14,7 @@
/>
</div>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<ng-container *ngIf="!hasMasterPassword">
<div class="box-content-row" appBoxRow>
<label class="d-block">{{ "sendVerificationCode" | i18n }}</label>
<button

View File

@ -646,3 +646,37 @@ main {
}
}
}
#login-initiated {
.margin-auto {
margin: auto;
}
.mb-20px {
margin-bottom: 20px;
}
.mx-5px {
margin-left: 5px !important;
margin-right: 5px !important;
}
.muted-hr {
margin-top: 1rem;
margin-bottom: 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.standard-x-margin {
margin-left: 5px;
margin-right: 5px;
}
.btn-top-margin {
margin-top: 12px;
}
#rememberThisDeviceHintText {
font-size: $font-size-small;
color: $text-muted;
}
}

View File

@ -1,21 +1,21 @@
import { APP_INITIALIZER, LOCALE_ID, NgModule } from "@angular/core";
import { LockGuard as BaseLockGuardService } from "@bitwarden/angular/auth/guards/lock.guard";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards/unauth.guard";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
import { MEMORY_STORAGE, SECURE_STORAGE } from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ThemingService } from "@bitwarden/angular/services/theming/theming.service";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import {
@ -24,7 +24,9 @@ import {
} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
@ -84,7 +86,7 @@ import { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export"
import { BrowserOrganizationService } from "../../admin-console/services/browser-organization.service";
import { BrowserPolicyService } from "../../admin-console/services/browser-policy.service";
import { LockGuardService, UnauthGuardService } from "../../auth/popup/services";
import { UnauthGuardService } from "../../auth/popup/services";
import { AutofillService } from "../../autofill/services/abstractions/autofill.service";
import MainBackground from "../../background/main.background";
import { Account } from "../../models/account";
@ -144,7 +146,6 @@ function getBgService<T>(service: keyof MainBackground) {
deps: [InitService],
multi: true,
},
{ provide: BaseLockGuardService, useClass: LockGuardService },
{ provide: BaseUnauthGuardService, useClass: UnauthGuardService },
{ provide: PopupUtilsService, useFactory: () => new PopupUtilsService(isPrivateMode) },
{
@ -252,6 +253,21 @@ function getBgService<T>(service: keyof MainBackground) {
},
deps: [EncryptService],
},
{
provide: AuthRequestCryptoServiceAbstraction,
useFactory: getBgService<AuthRequestCryptoServiceAbstraction>("authRequestCryptoService"),
deps: [],
},
{
provide: DeviceTrustCryptoServiceAbstraction,
useFactory: getBgService<DeviceTrustCryptoServiceAbstraction>("deviceTrustCryptoService"),
deps: [],
},
{
provide: DevicesServiceAbstraction,
useFactory: getBgService<DevicesServiceAbstraction>("devicesService"),
deps: [],
},
{
provide: EventUploadService,
useFactory: getBgService<EventUploadService>("eventUploadService"),

View File

@ -71,28 +71,29 @@
<div class="box-content-row display-block" appBoxRow>
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
<select
#vaultTimeoutActionSelect
id="vaultTimeoutAction"
name="VaultTimeoutActions"
formControlName="vaultTimeoutAction"
>
<option *ngFor="let o of vaultTimeoutActionOptions" [ngValue]="o.value">
{{ o.name }}
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
{{ action | i18n }}
</option>
</select>
</div>
<div
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
id="unlockMethodHelp"
class="box-footer"
>
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
<input id="pin" type="checkbox" (change)="updatePin()" formControlName="pin" />
<input id="pin" type="checkbox" formControlName="pin" />
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
<input
id="biometric"
type="checkbox"
(change)="updateBiometric()"
formControlName="biometric"
/>
<input id="biometric" type="checkbox" formControlName="biometric" />
</div>
<div
class="box-content-row box-content-row-checkbox"
@ -108,6 +109,7 @@
/>
</div>
<button
*ngIf="availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick

View File

@ -1,15 +1,28 @@
import { Component, ElementRef, OnInit, ViewChild } from "@angular/core";
import { ChangeDetectorRef, Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import { concatMap, debounceTime, filter, map, Observable, Subject, takeUntil, tap } from "rxjs";
import {
BehaviorSubject,
combineLatest,
concatMap,
distinctUntilChanged,
filter,
firstValueFrom,
map,
Observable,
pairwise,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import Swal from "sweetalert2";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -47,16 +60,15 @@ const RateUrls = {
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SettingsComponent implements OnInit {
@ViewChild("vaultTimeoutActionSelect", { read: ElementRef, static: true })
vaultTimeoutActionSelectRef: ElementRef;
protected readonly VaultTimeoutAction = VaultTimeoutAction;
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
vaultTimeoutOptions: any[];
vaultTimeoutActionOptions: any[];
vaultTimeoutPolicyCallout: Observable<{
timeout: { hours: number; minutes: number };
action: VaultTimeoutAction;
}>;
supportsBiometric: boolean;
previousVaultTimeout: number = null;
showChangeMasterPass = true;
form = this.formBuilder.group({
@ -67,6 +79,7 @@ export class SettingsComponent implements OnInit {
enableAutoBiometricsPrompt: true,
});
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
private destroy$ = new Subject<void>();
constructor(
@ -83,12 +96,14 @@ export class SettingsComponent implements OnInit {
private stateService: StateService,
private popupUtilsService: PopupUtilsService,
private modalService: ModalService,
private keyConnectorService: KeyConnectorService,
private dialogService: DialogService
private userVerificationService: UserVerificationService,
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef
) {}
async ngOnInit() {
this.vaultTimeoutPolicyCallout = this.policyService.get$(PolicyType.MaximumVaultTimeout).pipe(
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
this.vaultTimeoutPolicyCallout = maximumVaultTimeoutPolicy.pipe(
filter((policy) => policy != null),
map((policy) => {
let timeout;
@ -99,13 +114,6 @@ export class SettingsComponent implements OnInit {
};
}
return { timeout: timeout, action: policy.data?.action };
}),
tap((policy) => {
if (policy.action) {
this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
} else {
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
}
})
);
@ -131,35 +139,17 @@ export class SettingsComponent implements OnInit {
this.vaultTimeoutOptions.push({ name: this.i18nService.t("onRestart"), value: -1 });
this.vaultTimeoutOptions.push({ name: this.i18nService.t("never"), value: null });
this.vaultTimeoutActionOptions = [
{ name: this.i18nService.t(VaultTimeoutAction.Lock), value: VaultTimeoutAction.Lock },
{ name: this.i18nService.t(VaultTimeoutAction.LogOut), value: VaultTimeoutAction.LogOut },
];
let timeout = await this.vaultTimeoutSettingsService.getVaultTimeout();
if (timeout === -2 && !showOnLocked) {
timeout = -1;
}
const pinSet = await this.vaultTimeoutSettingsService.isPinLockSet();
const initialValues = {
vaultTimeout: timeout,
vaultTimeoutAction: await this.vaultTimeoutSettingsService.getVaultTimeoutAction(),
pin: pinSet[0] || pinSet[1],
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
enableAutoBiometricsPrompt: !(await this.stateService.getDisableAutoBiometricsPrompt()),
};
this.form.setValue(initialValues, { emitEvent: false });
this.previousVaultTimeout = timeout;
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
this.showChangeMasterPass = !(await this.keyConnectorService.getUsesKeyConnector());
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
this.form.controls.vaultTimeout.valueChanges
.pipe(
debounceTime(250),
concatMap(async (value) => {
await this.saveVaultTimeout(value);
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeout(previousValue, newValue);
}),
takeUntil(this.destroy$)
)
@ -167,25 +157,94 @@ export class SettingsComponent implements OnInit {
this.form.controls.vaultTimeoutAction.valueChanges
.pipe(
concatMap(async (action) => {
await this.saveVaultTimeoutAction(action);
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeoutAction(previousValue, newValue);
}),
takeUntil(this.destroy$)
)
.subscribe();
const initialValues = {
vaultTimeout: timeout,
vaultTimeoutAction: await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$()
),
pin: pinStatus !== "DISABLED",
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
enableAutoBiometricsPrompt: !(await this.stateService.getDisableAutoBiometricsPrompt()),
};
this.form.patchValue(initialValues); // Emit event to initialize `pairwise` operator
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
this.form.controls.pin.valueChanges
.pipe(
concatMap(async (value) => {
await this.updatePin(value);
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$)
)
.subscribe();
this.form.controls.biometric.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((enabled) => {
if (enabled) {
this.form.controls.enableAutoBiometricsPrompt.enable();
.pipe(
distinctUntilChanged(),
concatMap(async (enabled) => {
await this.updateBiometric(enabled);
if (enabled) {
this.form.controls.enableAutoBiometricsPrompt.enable();
} else {
this.form.controls.enableAutoBiometricsPrompt.disable();
}
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$)
)
.subscribe();
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
])
),
takeUntil(this.destroy$)
)
.subscribe(([availableActions, action]) => {
this.availableVaultTimeoutActions = availableActions;
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
// NOTE: The UI doesn't properly update without detect changes.
// I've even tried using an async pipe, but it still doesn't work. I'm not sure why.
// Using an async pipe means that we can't call `detectChanges` AFTER the data has change
// meaning that we are forced to use regular class variables instead of observables.
this.changeDetectorRef.detectChanges();
});
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
maximumVaultTimeoutPolicy,
])
),
takeUntil(this.destroy$)
)
.subscribe(([availableActions, policy]) => {
if (policy?.data?.action || availableActions.length <= 1) {
this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
} else {
this.form.controls.enableAutoBiometricsPrompt.disable();
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
}
});
}
async saveVaultTimeout(newValue: number) {
async saveVaultTimeout(previousValue: number, newValue: number) {
if (newValue == null) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
@ -194,7 +253,7 @@ export class SettingsComponent implements OnInit {
});
if (!confirmed) {
this.form.controls.vaultTimeout.setValue(this.previousVaultTimeout);
this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false });
return;
}
}
@ -210,18 +269,16 @@ export class SettingsComponent implements OnInit {
return;
}
this.previousVaultTimeout = this.form.value.vaultTimeout;
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
newValue,
this.form.value.vaultTimeoutAction
);
if (this.previousVaultTimeout == null) {
if (newValue == null) {
this.messagingService.send("bgReseedStorage");
}
}
async saveVaultTimeoutAction(newValue: VaultTimeoutAction) {
async saveVaultTimeoutAction(previousValue: VaultTimeoutAction, newValue: VaultTimeoutAction) {
if (newValue === VaultTimeoutAction.LogOut) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
@ -230,13 +287,7 @@ export class SettingsComponent implements OnInit {
});
if (!confirmed) {
this.vaultTimeoutActionOptions.forEach((option: any, i) => {
if (option.value === this.form.value.vaultTimeoutAction) {
this.vaultTimeoutActionSelectRef.nativeElement.value =
i + ": " + this.form.value.vaultTimeoutAction;
}
});
this.form.controls.vaultTimeoutAction.patchValue(VaultTimeoutAction.Lock, {
this.form.controls.vaultTimeoutAction.setValue(previousValue, {
emitEvent: false,
});
return;
@ -256,26 +307,26 @@ export class SettingsComponent implements OnInit {
this.form.value.vaultTimeout,
newValue
);
this.refreshTimeoutSettings$.next();
}
async updatePin() {
if (this.form.value.pin) {
async updatePin(value: boolean) {
if (value) {
const ref = this.modalService.open(SetPinComponent, { allowMultipleModals: true });
if (ref == null) {
this.form.controls.pin.setValue(false);
this.form.controls.pin.setValue(false, { emitEvent: false });
return;
}
this.form.controls.pin.setValue(await ref.onClosedPromise());
this.form.controls.pin.setValue(await ref.onClosedPromise(), { emitEvent: false });
} else {
await this.cryptoService.clearPinProtectedKey();
await this.vaultTimeoutSettingsService.clear();
}
}
async updateBiometric() {
if (this.form.value.biometric && this.supportsBiometric) {
async updateBiometric(enabled: boolean) {
if (enabled && this.supportsBiometric) {
let granted;
try {
granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] });
@ -324,7 +375,7 @@ export class SettingsComponent implements OnInit {
});
await this.stateService.setBiometricAwaitingAcceptance(true);
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
await Promise.race([
submitted.then(async (result) => {
@ -339,7 +390,7 @@ export class SettingsComponent implements OnInit {
this.form.controls.biometric.setValue(result);
Swal.close();
if (this.form.value.biometric === false) {
if (!result) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorEnableBiometricTitle"),

View File

@ -19,11 +19,11 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg)
let response = NSExtensionItem()
guard let command = message?["command"] as? String else {
return
}
switch (command) {
case "readFromClipboard":
let pasteboard = NSPasteboard.general
@ -59,12 +59,12 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
guard let data = blobData else {
return
}
let panel = NSSavePanel()
panel.canCreateDirectories = true
panel.nameFieldStringValue = dlMsg.fileName
let response = panel.runModal();
if response == NSApplication.ModalResponse.OK {
if let url = panel.url {
do {
@ -87,12 +87,12 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
}
return
case "biometricUnlock":
var error: NSError?
let laContext = LAContext()
laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if let e = error, e.code != kLAErrorBiometryLockout {
response.userInfo = [
SFExtensionMessageKey: [
@ -123,26 +123,32 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
guard let userId = message?["userId"] as? String else {
return
}
let passwordName = userId + "_masterkey_biometric"
let passwordName = userId + "_user_biometric"
var passwordLength: UInt32 = 0
var passwordPtr: UnsafeMutableRawPointer? = nil
var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil)
if status != errSecSuccess {
let fallbackName = "key"
status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil)
}
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3473)
if status != errSecSuccess {
let secondaryFallbackName = "_masterkey_biometric"
status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(secondaryFallbackName.utf8.count), secondaryFallbackName, &passwordLength, &passwordPtr, nil)
}
if status == errSecSuccess {
let result = NSString(bytes: passwordPtr!, length: Int(passwordLength), encoding: String.Encoding.utf8.rawValue) as String?
SecKeychainItemFreeContent(nil, passwordPtr)
response.userInfo = [ SFExtensionMessageKey: [
"message": [
"command": "biometricUnlock",
"response": "unlocked",
"timestamp": Int64(NSDate().timeIntervalSince1970 * 1000),
"keyB64": result!.replacingOccurrences(of: "\"", with: ""),
"userKeyB64": result!.replacingOccurrences(of: "\"", with: ""),
],
]]
} else {
@ -157,10 +163,10 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
]
}
}
context.completeRequest(returningItems: [response], completionHandler: nil)
}
return
default:
return

View File

@ -1,4 +1,4 @@
import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/services/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
import { SafariApp } from "../../browser/safariApp";

View File

@ -1,4 +1,4 @@
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { Response } from "../../models/response";
import { MessageResponse } from "../../models/response/message.response";

View File

@ -406,15 +406,15 @@ export class LoginCommand {
}
try {
const { newPasswordHash, newEncKey, hint } = await this.collectNewMasterPasswordDetails(
const { newPasswordHash, newUserKey, hint } = await this.collectNewMasterPasswordDetails(
"Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now."
);
const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(currentPassword, null);
request.masterPasswordHash = await this.cryptoService.hashMasterKey(currentPassword, null);
request.masterPasswordHint = hint;
request.newMasterPasswordHash = newPasswordHash;
request.key = newEncKey[1].encryptedString;
request.key = newUserKey[1].encryptedString;
await this.apiService.postPassword(request);
@ -444,12 +444,12 @@ export class LoginCommand {
}
try {
const { newPasswordHash, newEncKey, hint } = await this.collectNewMasterPasswordDetails(
const { newPasswordHash, newUserKey, hint } = await this.collectNewMasterPasswordDetails(
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now."
);
const request = new UpdateTempPasswordRequest();
request.key = newEncKey[1].encryptedString;
request.key = newUserKey[1].encryptedString;
request.newMasterPasswordHash = newPasswordHash;
request.masterPasswordHint = hint;
@ -467,8 +467,8 @@ export class LoginCommand {
/**
* Collect new master password and hint from the CLI. The collected password
* is validated against any applicable master password policies and a new encryption
* key is generated
* is validated against any applicable master password policies, a new master
* key is generated, and we use it to re-encrypt the user key
* @param prompt - Message that is displayed during the initial prompt
* @param error
*/
@ -477,7 +477,7 @@ export class LoginCommand {
error?: string
): Promise<{
newPasswordHash: string;
newEncKey: [SymmetricCryptoKey, EncString];
newUserKey: [SymmetricCryptoKey, EncString];
hint?: string;
}> {
if (this.email == null || this.email === "undefined") {
@ -559,21 +559,24 @@ export class LoginCommand {
const kdfConfig = await this.stateService.getKdfConfig();
// Create new key and hash new password
const newKey = await this.cryptoService.makeKey(
const newMasterKey = await this.cryptoService.makeMasterKey(
masterPassword,
this.email.trim().toLowerCase(),
kdf,
kdfConfig
);
const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey);
const newPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, newMasterKey);
// Grab user's current enc key
const userEncKey = await this.cryptoService.getEncKey();
// Grab user key
const userKey = await this.cryptoService.getUserKey();
if (!userKey) {
throw new Error("User key not found.");
}
// Create new encKey for the User
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
// Re-encrypt user key with new master key
const newUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(newMasterKey, userKey);
return { newPasswordHash, newEncKey, hint: masterPasswordHint };
return { newPasswordHash, newUserKey: newUserKey, hint: masterPasswordHint };
}
private async handleCaptchaRequired(

View File

@ -44,17 +44,17 @@ export class UnlockCommand {
const email = await this.stateService.getEmail();
const kdf = await this.stateService.getKdfType();
const kdfConfig = await this.stateService.getKdfConfig();
const key = await this.cryptoService.makeKey(password, email, kdf, kdfConfig);
const storedKeyHash = await this.cryptoService.getKeyHash();
const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig);
const storedKeyHash = await this.cryptoService.getMasterKeyHash();
let passwordValid = false;
if (key != null) {
if (masterKey != null) {
if (storedKeyHash != null) {
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, key);
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, masterKey);
} else {
const serverKeyHash = await this.cryptoService.hashPassword(
const serverKeyHash = await this.cryptoService.hashMasterKey(
password,
key,
masterKey,
HashPurpose.ServerAuthorization
);
const request = new SecretVerificationRequest();
@ -62,12 +62,12 @@ export class UnlockCommand {
try {
await this.apiService.postAccountVerifyPassword(request);
passwordValid = true;
const localKeyHash = await this.cryptoService.hashPassword(
const localKeyHash = await this.cryptoService.hashMasterKey(
password,
key,
masterKey,
HashPurpose.LocalAuthorization
);
await this.cryptoService.setKeyHash(localKeyHash);
await this.cryptoService.setMasterKeyHash(localKeyHash);
} catch {
// Ignore
}
@ -75,7 +75,9 @@ export class UnlockCommand {
}
if (passwordValid) {
await this.cryptoService.setKey(key);
await this.cryptoService.setMasterKey(masterKey);
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
if (await this.keyConnectorService.getConvertAccountRequired()) {
const convertToKeyConnectorCommand = new ConvertToKeyConnectorCommand(

View File

@ -12,7 +12,13 @@ import { OrganizationService } from "@bitwarden/common/admin-console/services/or
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
@ -38,8 +44,8 @@ import { OrganizationUserServiceImplementation } from "@bitwarden/common/service
import { SearchService } from "@bitwarden/common/services/search.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { TotpService } from "@bitwarden/common/services/totp.service";
import { VaultTimeoutService } from "@bitwarden/common/services/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
import {
PasswordGenerationService,
PasswordGenerationServiceAbstraction,
@ -140,6 +146,9 @@ export class Main {
organizationApiService: OrganizationApiServiceAbstraction;
syncNotifierService: SyncNotifierService;
sendApiService: SendApiService;
devicesApiService: DevicesApiServiceAbstraction;
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction;
authRequestCryptoService: AuthRequestCryptoServiceAbstraction;
constructor() {
let p = null;
@ -315,6 +324,20 @@ export class Main {
this.stateService
);
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
this.deviceTrustCryptoService = new DeviceTrustCryptoService(
this.cryptoFunctionService,
this.cryptoService,
this.encryptService,
this.stateService,
this.appIdService,
this.devicesApiService,
this.i18nService,
this.platformUtilsService
);
this.authRequestCryptoService = new AuthRequestCryptoServiceImplementation(this.cryptoService);
this.authService = new AuthService(
this.cryptoService,
this.apiService,
@ -330,17 +353,27 @@ export class Main {
this.i18nService,
this.encryptService,
this.passwordStrengthService,
this.policyService
this.policyService,
this.deviceTrustCryptoService,
this.authRequestCryptoService
);
const lockedCallback = async () =>
await this.cryptoService.clearStoredKey(KeySuffixOptions.Auto);
const lockedCallback = async (userId?: string) =>
await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto);
this.userVerificationService = new UserVerificationService(
this.stateService,
this.cryptoService,
this.i18nService,
this.userVerificationApiService
);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.cryptoService,
this.tokenService,
this.policyService,
this.stateService
this.stateService,
this.userVerificationService
);
this.vaultTimeoutService = new VaultTimeoutService(
@ -351,7 +384,6 @@ export class Main {
this.platformUtilsService,
this.messagingService,
this.searchService,
this.keyConnectorService,
this.stateService,
this.authService,
this.vaultTimeoutSettingsService,
@ -406,12 +438,6 @@ export class Main {
this.sendProgram = new SendProgram(this);
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
this.userVerificationService = new UserVerificationService(
this.cryptoService,
this.i18nService,
this.userVerificationApiService
);
}
async run() {

View File

@ -5,7 +5,6 @@ import * as koa from "koa";
import * as koaBodyParser from "koa-bodyparser";
import * as koaJson from "koa-json";
import { KeySuffixOptions } from "@bitwarden/common/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ConfirmCommand } from "../admin-console/commands/confirm.command";
@ -425,11 +424,7 @@ export class ServeCommand {
this.processResponse(res, Response.error("You are not logged in."));
return true;
}
if (await this.main.cryptoService.hasKeyInMemory()) {
return false;
} else if (await this.main.cryptoService.hasKeyStored(KeySuffixOptions.Auto)) {
// load key into memory
await this.main.cryptoService.getKey();
if (await this.main.cryptoService.hasUserKey()) {
return false;
}
this.processResponse(res, Response.error("Vault is locked."));

View File

@ -2,7 +2,6 @@ import * as chalk from "chalk";
import * as program from "commander";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { KeySuffixOptions } from "@bitwarden/common/enums";
import { LockCommand } from "./auth/commands/lock.command";
import { LoginCommand } from "./auth/commands/login.command";
@ -597,11 +596,8 @@ export class Program {
protected async exitIfLocked() {
await this.exitIfNotAuthed();
if (await this.main.cryptoService.hasKeyInMemory()) {
if (await this.main.cryptoService.hasUserKey()) {
return;
} else if (await this.main.cryptoService.hasKeyStored(KeySuffixOptions.Auto)) {
// load key into memory
await this.main.cryptoService.getKey();
} else if (process.env.BW_NOINTERACTION !== "true") {
// must unlock
if (await this.main.keyConnectorService.getUsesKeyConnector()) {

View File

@ -126,8 +126,8 @@ export class CreateCommand {
return Response.error("Premium status is required to use this feature.");
}
const encKey = await this.cryptoService.getEncKey();
if (encKey == null) {
const userKey = await this.cryptoService.getUserKey();
if (userKey == null) {
return Response.error(
"You must update your encryption key before you can use this feature. " +
"See https://help.bitwarden.com/article/update-encryption-key/"

View File

@ -55,43 +55,69 @@
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
></app-vault-timeout-input>
<div class="form-group">
<label>{{ "vaultTimeoutAction" | i18n }}</label>
<div class="radio radio-mt-2">
<label for="vaultTimeoutActionLock">
<input
type="radio"
id="vaultTimeoutActionLock"
value="{{ VaultTimeoutAction.Lock }}"
aria-describedby="vaultTimeoutActionLockHelp"
formControlName="vaultTimeoutAction"
/>
{{ "lock" | i18n }}
</label>
<ng-container
*ngIf="availableVaultTimeoutActions$ | async as availableVaultTimeoutActions"
>
<div class="form-group">
<label>{{ "vaultTimeoutAction" | i18n }}</label>
<div class="radio radio-mt-2">
<label for="vaultTimeoutActionLock">
<!--
Using [attr.disabled] because reactive forms don't support disabling individual radio buttons, see:
https://github.com/angular/angular/issues/11763
-->
<input
type="radio"
id="vaultTimeoutActionLock"
value="{{ VaultTimeoutAction.Lock }}"
aria-describedby="vaultTimeoutActionLockHelp"
formControlName="vaultTimeoutAction"
[attr.disabled]="
!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
? true
: null
"
/>
{{ "lock" | i18n }}
</label>
</div>
<small id="vaultTimeoutActionLockHelp" class="help-block">{{
"vaultTimeoutActionLockDesc" | i18n
}}</small>
<div class="radio">
<label for="vaultTimeoutActionLogOut">
<input
type="radio"
id="vaultTimeoutActionLogOut"
value="{{ VaultTimeoutAction.LogOut }}"
aria-describedby="vaultTimeoutActionLogOutHelp"
formControlName="vaultTimeoutAction"
[attr.disabled]="
!availableVaultTimeoutActions.includes(VaultTimeoutAction.LogOut)
? true
: null
"
/>
{{ "logOut" | i18n }}
</label>
</div>
<small id="vaultTimeoutActionLogOutHelp" class="help-block">{{
"vaultTimeoutActionLogOutDesc" | i18n
}}</small>
</div>
<small id="vaultTimeoutActionLockHelp" class="help-block">{{
"vaultTimeoutActionLockDesc" | i18n
}}</small>
<div class="radio">
<label for="vaultTimeoutActionLogOut">
<input
type="radio"
id="vaultTimeoutActionLogOut"
value="{{ VaultTimeoutAction.LogOut }}"
aria-describedby="vaultTimeoutActionLogOutHelp"
formControlName="vaultTimeoutAction"
/>
{{ "logOut" | i18n }}
</label>
<div
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
class="form-group"
>
<small id="unlockMethodNeededToChangeTimeoutActionHelp" class="help-block">{{
"unlockMethodNeededToChangeTimeoutActionDesc" | i18n
}}</small>
</div>
<small id="vaultTimeoutActionLogOutHelp" class="help-block">{{
"vaultTimeoutActionLogOutDesc" | i18n
}}</small>
</div>
</ng-container>
<div class="form-group">
<div class="checkbox">
<label for="pin">
<input id="pin" type="checkbox" formControlName="pin" (change)="updatePin()" />
<input id="pin" type="checkbox" formControlName="pin" />
{{ "unlockWithPin" | i18n }}
</label>
</div>
@ -99,12 +125,7 @@
<div class="form-group" *ngIf="supportsBiometric">
<div class="checkbox">
<label for="biometric">
<input
id="biometric"
type="checkbox"
formControlName="biometric"
(change)="updateBiometric()"
/>
<input id="biometric" type="checkbox" formControlName="biometric" />
{{ biometricText | i18n }}
</label>
</div>
@ -125,7 +146,14 @@
</label>
</div>
</div>
<div class="form-group" *ngIf="supportsBiometric && this.form.value.biometric">
<div
class="form-group"
*ngIf="
supportsBiometric &&
this.form.value.biometric &&
(userHasMasterPassword || (this.form.value.pin && userHasPinSet))
"
>
<div class="checkbox form-group-child">
<label for="requirePasswordOnStart">
<input

View File

@ -1,14 +1,15 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Observable, Subject } from "rxjs";
import { concatMap, debounceTime, filter, map, takeUntil, tap } from "rxjs/operators";
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType, ThemeType, KeySuffixOptions } from "@bitwarden/common/enums";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -22,7 +23,6 @@ import { flagEnabled } from "../../platform/flags";
import { ElectronStateService } from "../../platform/services/electron-state.service.abstraction";
import { isWindowsStore } from "../../utils";
import { SetPinComponent } from "../components/set-pin.component";
@Component({
selector: "app-settings",
templateUrl: "settings.component.html",
@ -61,12 +61,16 @@ export class SettingsComponent implements OnInit {
currentUserEmail: string;
availableVaultTimeoutActions$: Observable<VaultTimeoutAction[]>;
vaultTimeoutPolicyCallout: Observable<{
timeout: { hours: number; minutes: number };
action: "lock" | "logOut";
}>;
previousVaultTimeout: number = null;
userHasMasterPassword: boolean;
userHasPinSet: boolean;
form = this.formBuilder.group({
// Security
vaultTimeout: [null as number | null],
@ -97,6 +101,7 @@ export class SettingsComponent implements OnInit {
locale: [null as string | null],
});
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
private destroy$ = new Subject<void>();
constructor(
@ -111,7 +116,8 @@ export class SettingsComponent implements OnInit {
private modalService: ModalService,
private themingService: AbstractThemingService,
private settingsService: SettingsService,
private dialogService: DialogService
private dialogService: DialogService,
private userVerificationService: UserVerificationServiceAbstraction
) {
const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
@ -189,6 +195,8 @@ export class SettingsComponent implements OnInit {
}
async ngOnInit() {
this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();
this.isWindows = (await this.platformUtilsService.getDevice()) === DeviceType.WindowsDesktop;
if ((await this.stateService.getUserId()) == null) {
@ -196,6 +204,10 @@ export class SettingsComponent implements OnInit {
}
this.currentUserEmail = await this.stateService.getEmail();
this.availableVaultTimeoutActions$ = this.refreshTimeoutSettings$.pipe(
switchMap(() => this.vaultTimeoutSettingsService.availableVaultTimeoutActions$())
);
// Load timeout policy
this.vaultTimeoutPolicyCallout = this.policyService.get$(PolicyType.MaximumVaultTimeout).pipe(
filter((policy) => policy != null),
@ -219,11 +231,15 @@ export class SettingsComponent implements OnInit {
);
// Load initial values
const pinSet = await this.vaultTimeoutSettingsService.isPinLockSet();
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
this.userHasPinSet = pinStatus !== "DISABLED";
const initialValues = {
vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(),
vaultTimeoutAction: await this.vaultTimeoutSettingsService.getVaultTimeoutAction(),
pin: pinSet[0] || pinSet[1],
vaultTimeoutAction: await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$()
),
pin: this.userHasPinSet,
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
autoPromptBiometrics: !(await this.stateService.getDisableAutoBiometricsPrompt()),
requirePasswordOnStart:
@ -264,6 +280,15 @@ export class SettingsComponent implements OnInit {
this.autoPromptBiometricsText = await this.stateService.getNoAutoPromptBiometricsText();
this.previousVaultTimeout = this.form.value.vaultTimeout;
this.refreshTimeoutSettings$
.pipe(
switchMap(() => this.vaultTimeoutSettingsService.vaultTimeoutAction$()),
takeUntil(this.destroy$)
)
.subscribe((action) => {
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
});
// Form events
this.form.controls.vaultTimeout.valueChanges
.pipe(
@ -284,6 +309,26 @@ export class SettingsComponent implements OnInit {
)
.subscribe();
this.form.controls.pin.valueChanges
.pipe(
concatMap(async (value) => {
await this.updatePin(value);
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$)
)
.subscribe();
this.form.controls.biometric.valueChanges
.pipe(
concatMap(async (enabled) => {
await this.updateBiometric(enabled);
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$)
)
.subscribe();
this.form.controls.enableBrowserIntegration.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((enabled) => {
@ -362,52 +407,64 @@ export class SettingsComponent implements OnInit {
);
}
async updatePin() {
if (this.form.value.pin) {
async updatePin(value: boolean) {
if (value) {
const ref = this.modalService.open(SetPinComponent, { allowMultipleModals: true });
if (ref == null) {
this.form.controls.pin.setValue(false);
this.form.controls.pin.setValue(false, { emitEvent: false });
return;
}
this.form.controls.pin.setValue(await ref.onClosedPromise());
this.userHasPinSet = await ref.onClosedPromise();
this.form.controls.pin.setValue(this.userHasPinSet, { emitEvent: false });
}
if (!this.form.value.pin) {
await this.cryptoService.clearPinProtectedKey();
if (!value) {
// If user turned off PIN without having a MP and has biometric + require MP/PIN on restart enabled
if (this.form.value.requirePasswordOnStart && !this.userHasMasterPassword) {
// then must turn that off to prevent user from getting into bad state
this.form.controls.requirePasswordOnStart.setValue(false);
await this.updateRequirePasswordOnStart();
}
await this.vaultTimeoutSettingsService.clear();
}
this.messagingService.send("redrawMenu");
}
async updateBiometric() {
async updateBiometric(enabled: boolean) {
// NOTE: A bug in angular causes [ngModel] to not reflect the backing field value
// causing the checkbox to remain checked even if authentication fails.
// The bug should resolve itself once the angular issue is resolved.
// See: https://github.com/angular/angular/issues/13063
if (!this.form.value.biometric || !this.supportsBiometric) {
this.form.controls.biometric.setValue(false);
await this.stateService.setBiometricUnlock(null);
await this.cryptoService.toggleKey();
return;
}
try {
if (!enabled || !this.supportsBiometric) {
this.form.controls.biometric.setValue(false, { emitEvent: false });
await this.stateService.setBiometricUnlock(null);
await this.cryptoService.refreshAdditionalKeys();
return;
}
await this.stateService.setBiometricUnlock(true);
if (this.isWindows) {
// Recommended settings for Windows Hello
this.form.controls.requirePasswordOnStart.setValue(true);
this.form.controls.autoPromptBiometrics.setValue(false);
await this.stateService.setDisableAutoBiometricsPrompt(true);
await this.stateService.setBiometricRequirePasswordOnStart(true);
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
}
await this.cryptoService.toggleKey();
await this.stateService.setBiometricUnlock(true);
if (this.isWindows) {
// Recommended settings for Windows Hello
this.form.controls.requirePasswordOnStart.setValue(true);
this.form.controls.autoPromptBiometrics.setValue(false);
await this.stateService.setDisableAutoBiometricsPrompt(true);
await this.stateService.setBiometricRequirePasswordOnStart(true);
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
}
await this.cryptoService.refreshAdditionalKeys();
// Validate the key is stored in case biometrics fail.
const biometricSet = await this.cryptoService.hasKeyStored(KeySuffixOptions.Biometric);
this.form.controls.biometric.setValue(biometricSet);
if (!biometricSet) {
await this.stateService.setBiometricUnlock(null);
// Validate the key is stored in case biometrics fail.
const biometricSet = await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric);
this.form.controls.biometric.setValue(biometricSet, { emitEvent: false });
if (!biometricSet) {
await this.stateService.setBiometricUnlock(null);
}
} finally {
this.messagingService.send("redrawMenu");
}
}
@ -435,7 +492,7 @@ export class SettingsComponent implements OnInit {
await this.stateService.setBiometricEncryptionClientKeyHalf(null);
}
await this.stateService.setDismissedBiometricRequirePasswordOnStart();
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
}
async saveFavicons() {

View File

@ -1,13 +1,20 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards/auth.guard";
import { LockGuard } from "@bitwarden/angular/auth/guards/lock.guard";
import {
AuthGuard,
lockGuard,
redirectGuard,
tdeDecryptionRequiredGuard,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { LoginGuard } from "../auth/guards/login.guard";
import { HintComponent } from "../auth/hint.component";
import { LockComponent } from "../auth/lock.component";
import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component";
import { LoginWithDeviceComponent } from "../auth/login/login-with-device.component";
import { LoginComponent } from "../auth/login/login.component";
import { RegisterComponent } from "../auth/register.component";
@ -21,11 +28,16 @@ import { VaultComponent } from "../vault/app/vault/vault.component";
import { SendComponent } from "./tools/send/send.component";
const routes: Routes = [
{ path: "", redirectTo: "/vault", pathMatch: "full" },
{
path: "",
pathMatch: "full",
children: [], // Children lets us have an empty component.
canActivate: [redirectGuard({ loggedIn: "/vault", loggedOut: "/login", locked: "/lock" })],
},
{
path: "lock",
component: LockComponent,
canActivate: [LockGuard],
canActivate: [lockGuard()],
},
{
path: "login",
@ -36,7 +48,19 @@ const routes: Routes = [
path: "login-with-device",
component: LoginWithDeviceComponent,
},
{
path: "admin-approval-requested",
component: LoginWithDeviceComponent,
},
{ path: "2fa", component: TwoFactorComponent },
{
path: "login-initiated",
component: LoginDecryptionOptionsComponent,
canActivate: [
tdeDecryptionRequiredGuard(),
canAccessFeature(FeatureFlag.TrustedDeviceEncryption),
],
},
{ path: "register", component: RegisterComponent },
{
path: "vault",

View File

@ -20,13 +20,15 @@ import { EventUploadService } from "@bitwarden/common/abstractions/event/event-u
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -46,7 +48,7 @@ import { DialogService } from "@bitwarden/components";
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { LoginApprovalComponent } from "../auth/login/login-approval.component";
import { MenuUpdateRequest } from "../main/menu/menu.updater";
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
import { PremiumComponent } from "../vault/app/accounts/premium.component";
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
@ -77,6 +79,7 @@ const systemTimeoutOptions = {
<ng-template #appGenerator></ng-template>
<ng-template #loginApproval></ng-template>
<app-header></app-header>
<div id="container">
<div class="loading" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
@ -137,6 +140,7 @@ export class AppComponent implements OnInit, OnDestroy {
private policyService: InternalPolicyService,
private modalService: ModalService,
private keyConnectorService: KeyConnectorService,
private userVerificationService: UserVerificationService,
private configService: ConfigServiceAbstraction,
private dialogService: DialogService
) {}
@ -400,6 +404,9 @@ export class AppComponent implements OnInit, OnDestroy {
await this.openLoginApproval(message.notificationId);
}
break;
case "redrawMenu":
await this.updateAppMenu();
break;
}
});
});
@ -488,28 +495,32 @@ export class AppComponent implements OnInit, OnDestroy {
updateRequest = {
accounts: null,
activeUserId: null,
hideChangeMasterPassword: true,
};
} else {
const accounts: { [userId: string]: any } = {};
const accounts: { [userId: string]: MenuAccount } = {};
for (const i in stateAccounts) {
if (i != null && stateAccounts[i]?.profile?.userId != null) {
const userId = stateAccounts[i].profile.userId;
const availableTimeoutActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId)
);
accounts[userId] = {
isAuthenticated: await this.stateService.getIsAuthenticated({
userId: userId,
}),
isLocked:
(await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Locked,
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
email: stateAccounts[i].profile.email,
userId: stateAccounts[i].profile.userId,
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
};
}
}
updateRequest = {
accounts: accounts,
activeUserId: await this.stateService.getUserId(),
hideChangeMasterPassword: await this.keyConnectorService.getUsesKeyConnector(),
};
}

View File

@ -1,4 +1,4 @@
<ng-container *ngIf="!usesKeyConnector">
<ng-container *ngIf="hasMasterPassword">
<div class="box-content-row" appBoxRow>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
@ -14,7 +14,7 @@
/>
</div>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<ng-container *ngIf="!hasMasterPassword">
<div class="box-content-row" appBoxRow>
<label class="d-block">{{ "sendVerificationCode" | i18n }}</label>
<button

View File

@ -4,7 +4,6 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -14,7 +13,7 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { VaultTimeoutService } from "@bitwarden/common/services/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { I18nService } from "../../platform/services/i18n.service";
@ -26,7 +25,7 @@ export class InitService {
@Inject(WINDOW) private win: Window,
private environmentService: EnvironmentServiceAbstraction,
private syncService: SyncServiceAbstraction,
private vaultTimeoutService: VaultTimeoutServiceAbstraction,
private vaultTimeoutService: VaultTimeoutService,
private i18nService: I18nServiceAbstraction,
private eventUploadService: EventUploadServiceAbstraction,
private twoFactorService: TwoFactorServiceAbstraction,
@ -48,7 +47,7 @@ export class InitService {
// TODO: Remove this when implementing ticket PM-2637
this.environmentService.initialized = true;
this.syncService.fullSync(true);
(this.vaultTimeoutService as VaultTimeoutService).init(true);
await this.vaultTimeoutService.init(true);
const locale = await this.stateService.getLocale();
await (this.i18nService as I18nService).init(locale);
(this.eventUploadService as EventUploadService).init(true);

View File

@ -4,8 +4,12 @@
<p>{{ "yourVaultIsLocked" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow *ngIf="!hideInput">
<div class="row-main" *ngIf="pinLock">
<div
class="box-content-row box-content-row-flex"
appBoxRow
*ngIf="pinEnabled || masterPasswordEnabled"
>
<div class="row-main" *ngIf="pinEnabled">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
@ -17,7 +21,7 @@
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="!pinLock">
<div class="row-main" *ngIf="masterPasswordEnabled && !pinEnabled">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
@ -57,14 +61,14 @@
<button
type="button"
class="btn block"
[ngClass]="{ 'primary font-weight-bold': hideInput }"
[ngClass]="{ 'primary font-weight-bold': !pinEnabled && !masterPasswordEnabled }"
(click)="unlockBiometric()"
>
{{ biometricText | i18n }}
</button>
</div>
<div class="buttons-row">
<button type="submit" class="btn primary block" *ngIf="!hideInput">
<button type="submit" class="btn primary block" *ngIf="pinEnabled || masterPasswordEnabled">
<i class="bwi bwi-unlock" aria-hidden="true"></i> <b>{{ "unlock" | i18n }}</b>
</button>
<button type="button" class="btn block" (click)="logOut()">

View File

@ -4,11 +4,12 @@ import { ipcRenderer } from "electron";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType, KeySuffixOptions } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -51,8 +52,9 @@ export class LockComponent extends BaseLockComponent {
policyService: InternalPolicyService,
passwordStrengthService: PasswordStrengthServiceAbstraction,
logService: LogService,
keyConnectorService: KeyConnectorService,
dialogService: DialogService
dialogService: DialogService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
userVerificationService: UserVerificationService
) {
super(
router,
@ -66,12 +68,13 @@ export class LockComponent extends BaseLockComponent {
stateService,
apiService,
logService,
keyConnectorService,
ngZone,
policyApiService,
policyService,
passwordStrengthService,
dialogService
dialogService,
deviceTrustCryptoService,
userVerificationService
);
}
@ -131,7 +134,7 @@ export class LockComponent extends BaseLockComponent {
const userId = await this.stateService.getUserId();
const val = await ipcRenderer.invoke("biometric", {
action: BiometricStorageAction.EnabledForUser,
key: `${userId}_masterkey_biometric`,
key: `${userId}_user_biometric`,
keySuffix: KeySuffixOptions.Biometric,
userId: userId,
} as BiometricMessage);
@ -139,7 +142,7 @@ export class LockComponent extends BaseLockComponent {
}
private focusInput() {
document.getElementById(this.pinLock ? "pin" : "masterPassword").focus();
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
}
private async displayBiometricUpdateWarning(): Promise<void> {

View File

@ -0,0 +1,66 @@
<div id="login-decryption-options-page">
<div id="content" class="content">
<img class="logo-image" alt="Bitwarden" />
<div class="container loading-spinner" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<ng-container *ngIf="!loading">
<h1 id="heading">{{ "loginInitiated" | i18n }}</h1>
<h6
*ngIf="data.state == State.ExistingUserUntrustedDevice"
id="subHeading"
class="standard-bottom-margin"
>
{{ "deviceApprovalRequired" | i18n }}
</h6>
<form id="rememberDeviceForm" class="standard-bottom-margin" [formGroup]="rememberDeviceForm">
<div class="checkbox">
<label for="rememberDevice">
<input
id="rememberDevice"
type="checkbox"
name="rememberDevice"
formControlName="rememberDevice"
/>
{{ "rememberThisDevice" | i18n }}
</label>
</div>
<span id="rememberThisDeviceHintText">{{ "uncheckIfPublicDevice" | i18n }}</span>
</form>
<div *ngIf="data.state == State.ExistingUserUntrustedDevice" class="buttons with-rows">
<div class="buttons-row" *ngIf="data.showApproveFromOtherDeviceBtn">
<button (click)="approveFromOtherDevice()" type="button" class="btn primary block">
{{ "approveFromYourOtherDevice" | i18n }}
</button>
</div>
<div class="buttons-row" *ngIf="data.showReqAdminApprovalBtn">
<button (click)="requestAdminApproval()" type="button" class="btn block">
{{ "requestAdminApproval" | i18n }}
</button>
</div>
<div class="buttons-row" *ngIf="data.showApproveWithMasterPasswordBtn">
<button (click)="approveWithMasterPassword()" type="button" class="btn block">
{{ "approveWithMasterPassword" | i18n }}
</button>
</div>
</div>
<div *ngIf="data.state == State.NewUser" class="buttons with-rows">
<div class="buttons-row">
<button (click)="createUser()" type="button" class="btn block">
{{ "continue" | i18n }}
</button>
</div>
</div>
<div style="text-align: center">
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ data.userEmail }}</p>
<a [routerLink]="[]" (click)="logOut()">{{ "notYou" | i18n }}</a>
</div>
</ng-container>
</div>
</div>

View File

@ -0,0 +1,19 @@
import { Component } from "@angular/core";
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
@Component({
selector: "desktop-login-decryption-options",
templateUrl: "login-decryption-options.component.html",
})
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
override async createUser(): Promise<void> {
try {
await super.createUser();
this.messagingService.send("redrawMenu");
await this.router.navigate(["/vault"]);
} catch (error) {
this.validationService.showError(error);
}
}
}

View File

@ -1,40 +1,72 @@
<div id="login-with-device-page">
<div id="content" class="content">
<img class="logo-image" alt="Bitwarden" />
<p class="lead text-center">{{ "logInInitiated" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="section">
<p class="section">{{ "notificationSentDevice" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<p class="lead text-center">{{ "loginInitiated" | i18n }}</p>
<div class="fingerprint section">
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
<code>{{ fingerprintPhrase }}</code>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="section">
<p class="section">{{ "notificationSentDevice" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<div class="section" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<div class="fingerprint section">
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
<code>{{ fingerprintPhrase }}</code>
</div>
<div class="sub-options another-method">
<p class="no-margin description-text">
{{ "needAnotherOption" | i18n }}
<a type="button" class="text text-primary" (click)="goToLogin()">
{{ "viewAllLoginOptions" | i18n }}
</a>
</p>
<div class="section" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<div class="sub-options another-method">
<p class="no-margin description-text">
{{ "needAnotherOption" | i18n }}
<a type="button" class="text text-primary" (click)="back()">
{{ "viewAllLoginOptions" | i18n }}
</a>
</p>
</div>
</div>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
<p class="lead text-center">{{ "adminApprovalRequested" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="section">
<p class="section">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
<p class="section">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
</div>
<div class="fingerprint section">
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
<code>{{ fingerprintPhrase }}</code>
</div>
<div class="sub-options another-method">
<p class="no-margin description-text">
{{ "troubleLoggingIn" | i18n }}
<a type="button" class="text text-primary" (click)="back()">
{{ "viewAllLoginOptions" | i18n }}
</a>
</p>
</div>
</div>
</div>
</div>
</ng-container>
</div>
</div>
<ng-template #environment></ng-template>

View File

@ -1,3 +1,4 @@
import { Location } from "@angular/common";
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { Router } from "@angular/router";
@ -5,7 +6,9 @@ import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwa
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -50,7 +53,10 @@ export class LoginWithDeviceComponent
private modalService: ModalService,
syncService: SyncService,
stateService: StateService,
loginService: LoginService
loginService: LoginService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
authReqCryptoService: AuthRequestCryptoServiceAbstraction,
private location: Location
) {
super(
router,
@ -67,7 +73,9 @@ export class LoginWithDeviceComponent
anonymousHubService,
validationService,
stateService,
loginService
loginService,
deviceTrustCryptoService,
authReqCryptoService
);
super.onSuccessfulLogin = () => {
@ -100,7 +108,7 @@ export class LoginWithDeviceComponent
super.ngOnDestroy();
}
goToLogin() {
this.router.navigate(["/login"]);
back() {
this.location.back();
}
}

View File

@ -7,8 +7,8 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";

View File

@ -5,12 +5,18 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components
import { SharedModule } from "../../app/shared/shared.module";
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
import { LoginWithDeviceComponent } from "./login-with-device.component";
import { LoginComponent } from "./login.component";
@NgModule({
imports: [SharedModule, RouterModule],
declarations: [LoginComponent, LoginWithDeviceComponent, EnvironmentSelectorComponent],
declarations: [
LoginComponent,
LoginWithDeviceComponent,
EnvironmentSelectorComponent,
LoginDecryptionOptionsComponent,
],
exports: [LoginComponent, LoginWithDeviceComponent],
})
export class LoginModule {}

View File

@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -30,7 +31,8 @@ export class SsoComponent extends BaseSsoComponent {
cryptoFunctionService: CryptoFunctionService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
logService: LogService
logService: LogService,
configService: ConfigServiceAbstraction
) {
super(
authService,
@ -43,11 +45,17 @@ export class SsoComponent extends BaseSsoComponent {
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
logService,
configService
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
super.onSuccessfulLogin = async () => {
syncService.fullSync(true);
};
super.onSuccessfulLoginTde = async () => {
syncService.fullSync(true);
};
this.redirectUri = "bitwarden://sso-callback";
this.clientId = "desktop";
}

View File

@ -1,7 +1,8 @@
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { Component, Inject, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
@ -9,6 +10,7 @@ import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -43,7 +45,9 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
logService: LogService,
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService
loginService: LoginService,
configService: ConfigServiceAbstraction,
@Inject(WINDOW) protected win: Window
) {
super(
authService,
@ -51,18 +55,22 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
i18nService,
apiService,
platformUtilsService,
window,
win,
environmentService,
stateService,
route,
logService,
twoFactorService,
appIdService,
loginService
loginService,
configService
);
super.onSuccessfulLogin = () => {
this.loginService.clearValues();
return syncService.fullSync(true);
super.onSuccessfulLogin = async () => {
syncService.fullSync(true);
};
super.onSuccessfulLoginTde = async () => {
syncService.fullSync(true);
};
}

View File

@ -1492,6 +1492,9 @@
"vaultTimeoutActionLogOutDesc": {
"message": "Re-authentication is required to access your vault again."
},
"unlockMethodNeededToChangeTimeoutActionDesc": {
"message": "Set up an unlock method to change your vault timeout action."
},
"lock": {
"message": "Lock",
"description": "Verb form: to make secure or inaccesible by"
@ -2106,8 +2109,8 @@
"logInWithAnotherDevice": {
"message": "Log in with another device"
},
"logInInitiated": {
"message": "Log in initiated"
"loginInitiated": {
"message": "Login initiated"
},
"notificationSentDevice": {
"message": "A notification has been sent to your device."
@ -2246,6 +2249,34 @@
"windowsBiometricUpdateWarningTitle": {
"message": "Recommended Settings Update"
},
"deviceApprovalRequired": {
"message": "Device approval required. Select an approval option below:"
},
"rememberThisDevice": {
"message": "Remember this device"
},
"uncheckIfPublicDevice": {
"message": "Uncheck if using a public device"
},
"approveFromYourOtherDevice": {
"message": "Approve from your other device"
},
"requestAdminApproval": {
"message": "Request admin approval"
},
"approveWithMasterPassword": {
"message": "Approve with master password"
},
"region": {
"message": "Region"
},
"ssoIdentifierRequired": {
"message": "Organization SSO identifier is required."
},
"eu": {
"message": "EU",
"description": "European Union"
},
"loggingInOn": {
"message": "Logging in on"
},
@ -2260,5 +2291,29 @@
},
"accessDenied": {
"message": "Access denied. You do not have permission to view this page."
},
"accountSuccessfullyCreated": {
"message": "Account successfully created!"
},
"adminApprovalRequested": {
"message": "Admin approval requested"
},
"adminApprovalRequestSentToAdmins": {
"message": "Your request has been sent to your admin."
},
"youWillBeNotifiedOnceApproved": {
"message": "You will be notified once approved."
},
"troubleLoggingIn": {
"message": "Trouble logging in?"
},
"loginApproved": {
"message": "Login approved"
},
"userEmailMissing": {
"message": "User email missing"
},
"deviceTrusted": {
"message": "Device trusted"
}
}

View File

@ -15,14 +15,15 @@ export class AccountMenu implements IMenubarMenu {
}
get items(): MenuItemConstructorOptions[] {
return [
this.premiumMembership,
this.changeMasterPassword,
this.twoStepLogin,
this.fingerprintPhrase,
this.separator,
this.deleteAccount,
];
const items = [this.premiumMembership];
if (this._hasMasterPassword) {
items.push(this.changeMasterPassword);
}
items.push(this.twoStepLogin);
items.push(this.fingerprintPhrase);
items.push(this.separator);
items.push(this.deleteAccount);
return items;
}
private readonly _i18nService: I18nService;
@ -30,19 +31,22 @@ export class AccountMenu implements IMenubarMenu {
private readonly _webVaultUrl: string;
private readonly _window: BrowserWindow;
private readonly _isLocked: boolean;
private readonly _hasMasterPassword: boolean;
constructor(
i18nService: I18nService,
messagingService: MessagingService,
webVaultUrl: string,
window: BrowserWindow,
isLocked: boolean
isLocked: boolean,
hasMasterPassword: boolean
) {
this._i18nService = i18nService;
this._messagingService = messagingService;
this._webVaultUrl = webVaultUrl;
this._window = window;
this._isLocked = isLocked;
this._hasMasterPassword = hasMasterPassword;
}
private get premiumMembership(): MenuItemConstructorOptions {

View File

@ -52,9 +52,10 @@ export class BitwardenMenu extends FirstMenu implements IMenubarMenu {
updater: UpdaterMain,
window: BrowserWindow,
accounts: { [userId: string]: MenuAccount },
isLocked: boolean
isLocked: boolean,
isLockable: boolean
) {
super(i18nService, messagingService, updater, window, accounts, isLocked);
super(i18nService, messagingService, updater, window, accounts, isLocked, isLockable);
}
private get aboutBitwarden(): MenuItemConstructorOptions {

View File

@ -51,9 +51,10 @@ export class FileMenu extends FirstMenu implements IMenubarMenu {
updater: UpdaterMain,
window: BrowserWindow,
accounts: { [userId: string]: MenuAccount },
isLocked: boolean
isLocked: boolean,
isLockable: boolean
) {
super(i18nService, messagingService, updater, window, accounts, isLocked);
super(i18nService, messagingService, updater, window, accounts, isLocked, isLockable);
}
private get addNewLogin(): MenuItemConstructorOptions {

View File

@ -9,33 +9,24 @@ import { UpdaterMain } from "../updater.main";
import { MenuAccount } from "./menu.updater";
export class FirstMenu {
protected readonly _i18nService: I18nService;
protected readonly _updater: UpdaterMain;
protected readonly _messagingService: MessagingService;
protected readonly _accounts: { [userId: string]: MenuAccount };
protected readonly _window: BrowserWindow;
protected readonly _isLocked: boolean;
constructor(
i18nService: I18nService,
messagingService: MessagingService,
updater: UpdaterMain,
window: BrowserWindow,
accounts: { [userId: string]: MenuAccount },
isLocked: boolean
) {
this._i18nService = i18nService;
this._updater = updater;
this._messagingService = messagingService;
this._window = window;
this._accounts = accounts;
this._isLocked = isLocked;
}
protected readonly _i18nService: I18nService,
protected readonly _messagingService: MessagingService,
protected readonly _updater: UpdaterMain,
protected readonly _window: BrowserWindow,
protected readonly _accounts: { [userId: string]: MenuAccount },
protected readonly _isLocked: boolean,
protected readonly _isLockable: boolean
) {}
protected get hasAccounts(): boolean {
return this._accounts != null && Object.keys(this._accounts).length > 0;
}
protected get hasLockableAccounts(): boolean {
return this._accounts != null && Object.values(this._accounts).some((a) => a.isLockable);
}
protected get checkForUpdates(): MenuItemConstructorOptions {
return {
id: "checkForUpdates",
@ -66,23 +57,29 @@ export class FirstMenu {
id: "lock",
label: this.localize("lockVault"),
submenu: this.lockSubmenu,
enabled: this.hasAccounts,
enabled: this.hasLockableAccounts,
};
}
protected get lockSubmenu(): MenuItemConstructorOptions[] {
const value: MenuItemConstructorOptions[] = [];
for (const userId in this._accounts) {
if (userId == null) {
if (!userId) {
continue;
}
const account = this._accounts[userId];
if (account == null || !account.isLockable) {
continue;
}
value.push({
label: this._accounts[userId].email,
id: `lockNow_${this._accounts[userId].userId}`,
click: () => this.sendMessage("lockVault", { userId: this._accounts[userId].userId }),
enabled: !this._accounts[userId].isLocked,
visible: this._accounts[userId].isAuthenticated,
label: account.email,
id: `lockNow_${account.userId}`,
click: () => this.sendMessage("lockVault", { userId: account.userId }),
enabled: !account.isLocked,
visible: account.isAuthenticated,
});
}
return value;

View File

@ -1,5 +1,4 @@
export class MenuUpdateRequest {
hideChangeMasterPassword: boolean;
activeUserId: string;
accounts: { [userId: string]: MenuAccount };
}
@ -7,6 +6,8 @@ export class MenuUpdateRequest {
export class MenuAccount {
isAuthenticated: boolean;
isLocked: boolean;
isLockable: boolean;
userId: string;
email: string;
hasMasterPassword: boolean;
}

View File

@ -62,6 +62,10 @@ export class Menubar {
isLocked = updateRequest.accounts[updateRequest.activeUserId]?.isLocked ?? true;
}
const isLockable = !isLocked && updateRequest?.accounts[updateRequest.activeUserId]?.isLockable;
const hasMasterPassword =
updateRequest?.accounts[updateRequest.activeUserId]?.hasMasterPassword ?? false;
this.items = [
new FileMenu(
i18nService,
@ -69,11 +73,19 @@ export class Menubar {
updaterMain,
windowMain.win,
updateRequest?.accounts,
isLocked
isLocked,
isLockable
),
new EditMenu(i18nService, messagingService, isLocked),
new ViewMenu(i18nService, messagingService, isLocked),
new AccountMenu(i18nService, messagingService, webVaultUrl, windowMain.win, isLocked),
new AccountMenu(
i18nService,
messagingService,
webVaultUrl,
windowMain.win,
isLocked,
hasMasterPassword
),
new WindowMenu(i18nService, messagingService, windowMain),
new HelpMenu(
i18nService,
@ -91,7 +103,8 @@ export class Menubar {
updaterMain,
windowMain.win,
updateRequest?.accounts,
isLocked
isLocked,
isLockable
),
],
...this.items,

View File

@ -0,0 +1,91 @@
import { mock, mockReset } from "jest-mock-extended";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { ElectronCryptoService } from "./electron-crypto.service";
import { ElectronStateService } from "./electron-state.service.abstraction";
describe("electronCryptoService", () => {
let electronCryptoService: ElectronCryptoService;
const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>();
const platformUtilService = mock<PlatformUtilsService>();
const logService = mock<LogService>();
const stateService = mock<ElectronStateService>();
const mockUserId = "mock user id";
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
mockReset(platformUtilService);
mockReset(logService);
mockReset(stateService);
electronCryptoService = new ElectronCryptoService(
cryptoFunctionService,
encryptService,
platformUtilService,
logService,
stateService
);
});
it("instantiates", () => {
expect(electronCryptoService).not.toBeFalsy();
});
describe("setUserKey", () => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
describe("Biometric Key refresh", () => {
it("sets an Biometric key if getBiometricUnlock is true and the platform supports secure storage", async () => {
stateService.getBiometricUnlock.mockResolvedValue(true);
platformUtilService.supportsSecureStorage.mockReturnValue(true);
stateService.getBiometricRequirePasswordOnStart.mockResolvedValue(false);
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(
expect.objectContaining({ key: expect.any(String), clientEncKeyHalf: null }),
{
userId: mockUserId,
}
);
});
it("clears the Biometric key if getBiometricUnlock is false or the platform does not support secure storage", async () => {
stateService.getBiometricUnlock.mockResolvedValue(true);
platformUtilService.supportsSecureStorage.mockReturnValue(false);
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyBiometric).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
it("clears the old deprecated Biometric key whenever a User Key is set", async () => {
await electronCryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setCryptoMasterKeyBiometric).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
});
});
});

View File

@ -4,7 +4,12 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
import { CsprngString } from "@bitwarden/common/types/csprng";
@ -21,36 +26,80 @@ export class ElectronCryptoService extends CryptoService {
super(cryptoFunctionService, encryptService, platformUtilsService, logService, stateService);
}
protected override async storeKey(key: SymmetricCryptoKey, userId?: string) {
await super.storeKey(key, userId);
override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
if (keySuffix === KeySuffixOptions.Biometric) {
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3474)
const oldKey = await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId });
return oldKey || (await this.stateService.hasUserKeyBiometric({ userId: userId }));
}
return super.hasUserKeyStored(keySuffix, userId);
}
override async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise<void> {
if (keySuffix === KeySuffixOptions.Biometric) {
this.stateService.setUserKeyBiometric(null, { userId: userId });
this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
return;
}
super.clearStoredUserKey(keySuffix, userId);
}
protected override async storeAdditionalKeys(key: UserKey, userId?: string) {
await super.storeAdditionalKeys(key, userId);
const storeBiometricKey = await this.shouldStoreKey(KeySuffixOptions.Biometric, userId);
if (storeBiometricKey) {
await this.storeBiometricKey(key, userId);
} else {
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
await this.stateService.setUserKeyBiometric(null, { userId: userId });
}
await this.clearDeprecatedKeys(KeySuffixOptions.Biometric, userId);
}
protected async storeBiometricKey(key: SymmetricCryptoKey, userId?: string): Promise<void> {
protected override async getKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: string
): Promise<UserKey> {
if (keySuffix === KeySuffixOptions.Biometric) {
await this.migrateBiometricKeyIfNeeded(userId);
const userKey = await this.stateService.getUserKeyBiometric({ userId: userId });
return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey)) as UserKey;
}
return await super.getKeyFromStorage(keySuffix, userId);
}
protected async storeBiometricKey(key: UserKey, userId?: string): Promise<void> {
let clientEncKeyHalf: CsprngString = null;
if (await this.stateService.getBiometricRequirePasswordOnStart({ userId })) {
clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userId);
}
await this.stateService.setCryptoMasterKeyBiometric(
await this.stateService.setUserKeyBiometric(
{ key: key.keyB64, clientEncKeyHalf },
{ userId: userId }
);
}
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId?: string): Promise<boolean> {
if (keySuffix === KeySuffixOptions.Biometric) {
const biometricUnlock = await this.stateService.getBiometricUnlock({ userId: userId });
return biometricUnlock && this.platformUtilService.supportsSecureStorage();
}
return await super.shouldStoreKey(keySuffix, userId);
}
protected override async clearAllStoredUserKeys(userId?: string): Promise<void> {
await this.stateService.setUserKeyBiometric(null, { userId: userId });
super.clearAllStoredUserKeys(userId);
}
private async getBiometricEncryptionClientKeyHalf(userId?: string): Promise<CsprngString | null> {
try {
let biometricKey = await this.stateService
.getBiometricEncryptionClientKeyHalf({ userId })
.then((result) => result?.decrypt(null /* user encrypted */))
.then((result) => result as CsprngString);
const userKey = await this.getKeyForUserEncryption();
const userKey = await this.getUserKeyWithLegacySupport();
if (biometricKey == null && userKey != null) {
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
biometricKey = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
@ -63,4 +112,34 @@ export class ElectronCryptoService extends CryptoService {
return null;
}
}
// --LEGACY METHODS--
// We previously used the master key for additional keys, but now we use the user key.
// These methods support migrating the old keys to the new ones.
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475)
override async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string) {
if (keySuffix === KeySuffixOptions.Biometric) {
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
}
super.clearDeprecatedKeys(keySuffix, userId);
}
private async migrateBiometricKeyIfNeeded(userId?: string) {
if (await this.stateService.hasCryptoMasterKeyBiometric({ userId })) {
const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId });
// decrypt
const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey;
let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey();
encUserKey = encUserKey ?? (await this.stateService.getMasterKeyEncryptedUserKey());
if (!encUserKey) {
throw new Error("No user key found during biometric migration");
}
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey));
// migrate
await this.storeBiometricKey(userKey, userId);
await this.stateService.setCryptoMasterKeyBiometric(null, { userId });
}
}
}

View File

@ -98,6 +98,10 @@ export class ElectronStateService
options
);
if (b64DeviceKey == null) {
return null;
}
return new SymmetricCryptoKey(Utils.fromB64ToArray(b64DeviceKey)) as DeviceKey;
}

View File

@ -5,7 +5,8 @@
#lock-page,
#sso-page,
#set-password-page,
#remove-password-page {
#remove-password-page,
#login-decryption-options-page {
display: flex;
justify-content: center;
align-items: center;
@ -53,7 +54,8 @@
#hint-page,
#two-factor-page,
#lock-page,
#update-temp-password-page {
#update-temp-password-page,
#login-decryption-options-page {
.content {
width: 325px;
transition: width 0.25s linear;
@ -189,7 +191,8 @@
}
#login-page,
#login-with-device-page {
#login-with-device-page,
#login-decryption-options-page {
flex-direction: column;
justify-content: unset;
padding-top: 20px;
@ -273,3 +276,14 @@
}
}
}
#login-decryption-options-page {
.standard-bottom-margin {
margin-bottom: 20px;
}
#rememberThisDeviceHintText {
font-size: $font-size-small;
color: $text-muted;
}
}

View File

@ -8,7 +8,7 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { StateService } from "@bitwarden/common/platform/services/state.service";
@ -144,7 +144,9 @@ export class NativeMessageHandlerService {
}
private async handleEncryptedMessage(message: EncryptedMessage) {
message.encryptedCommand = EncString.fromJSON(message.encryptedCommand.toString());
message.encryptedCommand = EncString.fromJSON(
message.encryptedCommand.toString() as EncryptedString
);
const decryptedCommandData = await this.decryptPayload(message);
const { command } = decryptedCommandData;

View File

@ -136,14 +136,23 @@ export class NativeMessagingService {
});
}
const key = await this.cryptoService.getKeyFromStorage(
const userKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Biometric,
message.userId
);
const masterKey = await this.cryptoService.getMasterKey(message.userId);
if (key != null) {
if (userKey != null) {
// we send the master key still for backwards compatibility
// with older browser extensions
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472)
this.send(
{ command: "biometricUnlock", response: "unlocked", keyB64: key.keyB64 },
{
command: "biometricUnlock",
response: "unlocked",
keyB64: masterKey?.keyB64,
userKeyB64: userKey.keyB64,
},
appId
);
} else {

View File

@ -7,6 +7,7 @@ import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enum
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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BulkUserDetails } from "./bulk-status.component";
@ -98,7 +99,7 @@ export class BulkConfirmComponent implements OnInit {
);
}
protected getCryptoKey() {
protected getCryptoKey(): Promise<SymmetricCryptoKey> {
return this.cryptoService.getOrgKey(this.organizationId);
}

View File

@ -22,7 +22,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { DialogService } from "@bitwarden/components";
@ -171,26 +174,32 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
orgSymKey
);
// Decrypt User's Reset Password Key to get EncKey
// Decrypt User's Reset Password Key to get UserKey
const decValue = await this.cryptoService.rsaDecrypt(resetPasswordKey, decPrivateKey);
const userEncKey = new SymmetricCryptoKey(decValue);
const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey;
// Create new key and hash new password
const newKey = await this.cryptoService.makeKey(
// Create new master key and hash new password
const newMasterKey = await this.cryptoService.makeMasterKey(
this.newPassword,
this.email.trim().toLowerCase(),
kdfType,
new KdfConfig(kdfIterations, kdfMemory, kdfParallelism)
);
const newPasswordHash = await this.cryptoService.hashPassword(this.newPassword, newKey);
const newMasterKeyHash = await this.cryptoService.hashMasterKey(
this.newPassword,
newMasterKey
);
// Create new encKey for the User
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
// Create new encrypted user key for the User
const newUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(
newMasterKey,
existingUserKey
);
// Create request
const request = new OrganizationUserResetPasswordRequest();
request.key = newEncKey[1].encryptedString;
request.newMasterPasswordHash = newPasswordHash;
request.key = newUserKey[1].encryptedString;
request.newMasterPasswordHash = newMasterKeyHash;
// Change user's password
return this.organizationUserService.putOrganizationUserResetPassword(

View File

@ -1,7 +1,7 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards/auth.guard";
import { AuthGuard } from "@bitwarden/angular/auth/guards";
import {
canAccessOrgAdmin,
canAccessGroupsTab,

View File

@ -58,8 +58,8 @@ export class EnrollMasterPasswordReset {
const publicKey = Utils.fromB64ToArray(orgKeys.publicKey);
// RSA Encrypt user's encKey.key with organization public key
const encKey = await this.cryptoService.getEncKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey);
const userKey = await this.cryptoService.getUserKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
keyString = encryptedKey.encryptedString;
toastStringRef = "enrollPasswordResetSuccess";

View File

@ -11,7 +11,7 @@ import { EventUploadService } from "@bitwarden/common/abstractions/event/event-u
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";

View File

@ -18,6 +18,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrgKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BaseAcceptComponent } from "../common/base.accept.component";
@ -108,16 +109,14 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
const request = new OrganizationUserAcceptInitRequest();
request.token = qParams.token;
const [encryptedOrgShareKey, orgShareKey] = await this.cryptoService.makeShareKey();
const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(
orgShareKey
);
const [encryptedOrgKey, orgKey] = await this.cryptoService.makeOrgKey<OrgKey>();
const [orgPublicKey, encryptedOrgPrivateKey] = await this.cryptoService.makeKeyPair(orgKey);
const collection = await this.cryptoService.encrypt(
this.i18nService.t("defaultCollection"),
orgShareKey
orgKey
);
request.key = encryptedOrgShareKey.encryptedString;
request.key = encryptedOrgKey.encryptedString;
request.keys = new OrganizationKeysRequest(
orgPublicKey,
encryptedOrgPrivateKey.encryptedString
@ -141,8 +140,8 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user's encKey.key with organization public key
const encKey = await this.cryptoService.getEncKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey);
const userKey = await this.cryptoService.getUserKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
// Add reset password key to accept request
request.resetPasswordKey = encryptedKey.encryptedString;

View File

@ -3,11 +3,12 @@ import { Router } from "@angular/router";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -38,12 +39,13 @@ export class LockComponent extends BaseLockComponent {
stateService: StateService,
apiService: ApiService,
logService: LogService,
keyConnectorService: KeyConnectorService,
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordStrengthService: PasswordStrengthServiceAbstraction,
dialogService: DialogService
dialogService: DialogService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
userVerificationService: UserVerificationService
) {
super(
router,
@ -57,12 +59,13 @@ export class LockComponent extends BaseLockComponent {
stateService,
apiService,
logService,
keyConnectorService,
ngZone,
policyApiService,
policyService,
passwordStrengthService,
dialogService
dialogService,
deviceTrustCryptoService,
userVerificationService
);
}

View File

@ -0,0 +1,105 @@
<div class="tw-container tw-mx-auto">
<div
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
>
<div class="tw-mb-6">
<img class="logo logo-themed" alt="Bitwarden" />
</div>
<ng-container *ngIf="loading">
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</ng-container>
<div
*ngIf="!loading"
class="tw-w-full tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<ng-container *ngIf="data.state == State.ExistingUserUntrustedDevice">
<h2 bitTypography="h2" class="tw-mb-6">{{ "loginInitiated" | i18n }}</h2>
<p bitTypography="body1" class="tw-mb-6">
{{ "deviceApprovalRequired" | i18n }}
</p>
<form [formGroup]="rememberDeviceForm">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="rememberDevice" />
<bit-label>{{ "rememberThisDevice" | i18n }} </bit-label>
<bit-hint bitTypography="body2">{{ "uncheckIfPublicDevice" | i18n }}</bit-hint>
</bit-form-control>
</form>
<div class="tw-mb-6 tw-flex tw-flex-col tw-space-y-3">
<button
*ngIf="data.showApproveFromOtherDeviceBtn"
(click)="approveFromOtherDevice()"
bitButton
type="button"
buttonType="primary"
block
>
{{ "approveFromYourOtherDevice" | i18n }}
</button>
<button
*ngIf="data.showReqAdminApprovalBtn"
(click)="requestAdminApproval()"
bitButton
type="button"
buttonType="secondary"
>
{{ "requestAdminApproval" | i18n }}
</button>
<button
*ngIf="data.showApproveWithMasterPasswordBtn"
(click)="approveWithMasterPassword()"
bitButton
type="button"
buttonType="secondary"
block
>
{{ "approveWithMasterPassword" | i18n }}
</button>
</div>
</ng-container>
<ng-container *ngIf="data.state == State.NewUser">
<h2 bitTypography="h2" class="tw-mb-6">{{ "loggedInExclamation" | i18n }}</h2>
<form [formGroup]="rememberDeviceForm">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="rememberDevice" />
<bit-label>{{ "rememberThisDevice" | i18n }} </bit-label>
<bit-hint bitTypography="body2">{{ "uncheckIfPublicDevice" | i18n }}</bit-hint>
</bit-form-control>
</form>
<button
bitButton
type="button"
buttonType="primary"
block
class="tw-mb-6"
[bitAction]="createUserAction"
>
{{ "continue" | i18n }}
</button>
</ng-container>
<hr class="tw-mb-6 tw-mt-0" />
<div class="tw-m-0 tw-text-sm">
<p class="tw-mb-1">{{ "loggingInAs" | i18n }} {{ data.userEmail }}</p>
<a [routerLink]="[]" (click)="logOut()">{{ "notYou" | i18n }}</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
import { Component } from "@angular/core";
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
@Component({
selector: "web-login-decryption-options",
templateUrl: "login-decryption-options.component.html",
})
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
override async createUser(): Promise<void> {
try {
await super.createUser();
await this.router.navigate(["/vault"]);
} catch (error) {
this.validationService.showError(error);
}
}
createUserAction = async (): Promise<void> => {
return this.createUser();
};
}

View File

@ -5,42 +5,71 @@
>
<div>
<img class="logo logo-themed" alt="Bitwarden" />
<p class="tw-mx-4 tw-mb-4 tw-mt-3 tw-text-center tw-text-xl">
{{ "loginOrCreateNewAccount" | i18n }}
</p>
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInInitiated" | i18n }}</h2>
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<p class="tw-mx-4 tw-mb-4 tw-mt-3 tw-text-center tw-text-xl">
{{ "loginOrCreateNewAccount" | i18n }}
</p>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "loginInitiated" | i18n }}</h2>
<p class="tw-mb-6">
{{ "fingerprintMatchInfo" | i18n }}
</p>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
<p class="tw-mb-6">
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
<p>
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<div class="tw-my-10" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<hr />
<div class="tw-text-light tw-mt-3">
{{ "loginWithDeviceEnabledNote" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
</ng-container>
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "adminApprovalRequested" | i18n }}</h2>
<div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
<p>
<code>{{ fingerprintPhrase }}</code>
</p>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
<p class="tw-mb-6">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
</div>
<div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
<p>
<code>{{ fingerprintPhrase }}</code>
</p>
</div>
<hr />
<div class="tw-text-light tw-mt-3">
{{ "troubleLoggingIn" | i18n }}
<a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
<div class="tw-my-10" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<hr />
<div class="tw-text-light tw-mt-3">
{{ "loginWithDeviceEnabledNote" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
</ng-container>
</div>
</div>

View File

@ -4,7 +4,9 @@ import { Router } from "@angular/router";
import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-with-device.component";
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -41,7 +43,9 @@ export class LoginWithDeviceComponent
anonymousHubService: AnonymousHubService,
validationService: ValidationService,
stateService: StateService,
loginService: LoginService
loginService: LoginService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
authReqCryptoService: AuthRequestCryptoServiceAbstraction
) {
super(
router,
@ -58,7 +62,9 @@ export class LoginWithDeviceComponent
anonymousHubService,
validationService,
stateService,
loginService
loginService,
deviceTrustCryptoService,
authReqCryptoService
);
}
}

View File

@ -6,7 +6,6 @@ import { first } from "rxjs/operators";
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
@ -14,6 +13,7 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";

View File

@ -4,12 +4,13 @@ import { CheckboxModule } from "@bitwarden/components";
import { SharedModule } from "../../../app/shared";
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
import { LoginWithDeviceComponent } from "./login-with-device.component";
import { LoginComponent } from "./login.component";
@NgModule({
imports: [SharedModule, CheckboxModule],
declarations: [LoginComponent, LoginWithDeviceComponent],
exports: [LoginComponent, LoginWithDeviceComponent],
declarations: [LoginComponent, LoginWithDeviceComponent, LoginDecryptionOptionsComponent],
exports: [LoginComponent, LoginWithDeviceComponent, LoginDecryptionOptionsComponent],
})
export class LoginModule {}

View File

@ -35,7 +35,7 @@ export class RecoverTwoFactorComponent {
request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase();
request.email = this.email.trim().toLowerCase();
const key = await this.authService.makePreloginKey(this.masterPassword, request.email);
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
request.masterPasswordHash = await this.cryptoService.hashMasterKey(this.masterPassword, key);
this.formPromise = this.apiService.postTwoFactorRecover(request);
await this.formPromise;
this.platformUtilsService.showToast(

View File

@ -89,12 +89,12 @@
<input
class="form-check-input"
type="checkbox"
id="rotateEncKey"
name="RotateEncKey"
[(ngModel)]="rotateEncKey"
(change)="rotateEncKeyClicked()"
id="rotateUserKey"
name="RotateUserKey"
[(ngModel)]="rotateUserKey"
(change)="rotateUserKeyClicked()"
/>
<label class="form-check-label" for="rotateEncKey">
<label class="form-check-label" for="rotateUserKey">
{{ "rotateAccountEncKey" | i18n }}
</label>
<a

View File

@ -10,7 +10,9 @@ import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/commo
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EmergencyAccessStatusType } from "@bitwarden/common/auth/enums/emergency-access-status-type";
import { EmergencyAccessUpdateRequest } from "@bitwarden/common/auth/models/request/emergency-access-update.request";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
@ -22,7 +24,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
@ -38,7 +44,7 @@ import { DialogService } from "@bitwarden/components";
templateUrl: "change-password.component.html",
})
export class ChangePasswordComponent extends BaseChangePasswordComponent {
rotateEncKey = false;
rotateUserKey = false;
currentMasterPassword: string;
masterPasswordHint: string;
checkForBreaches = true;
@ -63,7 +69,9 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
private router: Router,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
dialogService: DialogService
dialogService: DialogService,
private userVerificationService: UserVerificationService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
) {
super(
i18nService,
@ -78,7 +86,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
}
async ngOnInit() {
if (await this.keyConnectorService.getUsesKeyConnector()) {
if (!(await this.userVerificationService.hasMasterPassword())) {
this.router.navigate(["/settings/security/two-factor"]);
}
@ -88,8 +96,8 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
}
async rotateEncKeyClicked() {
if (this.rotateEncKey) {
async rotateUserKeyClicked() {
if (this.rotateUserKey) {
const ciphers = await this.cipherService.getAllDecrypted();
let hasOldAttachments = false;
if (ciphers != null) {
@ -115,7 +123,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
"https://bitwarden.com/help/attachments/#add-storage-space"
);
}
this.rotateEncKey = false;
this.rotateUserKey = false;
return;
}
@ -131,14 +139,14 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
});
if (!result) {
this.rotateEncKey = false;
this.rotateUserKey = false;
}
}
}
async submit() {
const hasEncKey = await this.cryptoService.hasEncKey();
if (!hasEncKey) {
const hasUserKey = await this.cryptoService.hasUserKey();
if (!hasUserKey) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("updateKey"));
return;
}
@ -170,7 +178,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
return false;
}
if (this.rotateEncKey) {
if (this.rotateUserKey) {
await this.syncService.fullSync(true);
}
@ -179,22 +187,23 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
async performSubmitActions(
newMasterPasswordHash: string,
newKey: SymmetricCryptoKey,
newEncKey: [SymmetricCryptoKey, EncString]
newMasterKey: MasterKey,
newUserKey: [UserKey, EncString]
) {
const masterKey = await this.cryptoService.getOrDeriveMasterKey(this.currentMasterPassword);
const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
this.currentMasterPassword,
null
masterKey
);
request.masterPasswordHint = this.masterPasswordHint;
request.newMasterPasswordHash = newMasterPasswordHash;
request.key = newEncKey[1].encryptedString;
request.key = newUserKey[1].encryptedString;
try {
if (this.rotateEncKey) {
if (this.rotateUserKey) {
this.formPromise = this.apiService.postPassword(request).then(() => {
return this.updateKey(newKey, request.newMasterPasswordHash);
return this.updateKey(newMasterKey, request.newMasterPasswordHash);
});
} else {
this.formPromise = this.apiService.postPassword(request);
@ -213,16 +222,16 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
}
}
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {
const encKey = await this.cryptoService.makeEncKey(key);
const privateKey = await this.cryptoService.getPrivateKey();
private async updateKey(masterKey: MasterKey, masterPasswordHash: string) {
const [newUserKey, masterKeyEncUserKey] = await this.cryptoService.makeUserKey(masterKey);
const userPrivateKey = await this.cryptoService.getPrivateKey();
let encPrivateKey: EncString = null;
if (privateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(privateKey, encKey[0]);
if (userPrivateKey != null) {
encPrivateKey = await this.cryptoService.encrypt(userPrivateKey, newUserKey);
}
const request = new UpdateKeyRequest();
request.privateKey = encPrivateKey != null ? encPrivateKey.encryptedString : null;
request.key = encKey[1].encryptedString;
request.key = masterKeyEncUserKey.encryptedString;
request.masterPasswordHash = masterPasswordHash;
const folders = await firstValueFrom(this.folderService.folderViews$);
@ -230,7 +239,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
if (folders[i].id == null) {
continue;
}
const folder = await this.folderService.encrypt(folders[i], encKey[0]);
const folder = await this.folderService.encrypt(folders[i], newUserKey);
request.folders.push(new FolderWithIdRequest(folder));
}
@ -240,24 +249,26 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
continue;
}
const cipher = await this.cipherService.encrypt(ciphers[i], encKey[0]);
const cipher = await this.cipherService.encrypt(ciphers[i], newUserKey);
request.ciphers.push(new CipherWithIdRequest(cipher));
}
const sends = await firstValueFrom(this.sendService.sends$);
await Promise.all(
sends.map(async (send) => {
const cryptoKey = await this.cryptoService.decryptToBytes(send.key, null);
send.key = (await this.cryptoService.encrypt(cryptoKey, encKey[0])) ?? send.key;
const sendKey = await this.cryptoService.decryptToBytes(send.key, null);
send.key = (await this.cryptoService.encrypt(sendKey, newUserKey)) ?? send.key;
request.sends.push(new SendWithIdRequest(send));
})
);
await this.deviceTrustCryptoService.rotateDevicesTrust(newUserKey, masterPasswordHash);
await this.apiService.postAccountKey(request);
await this.updateEmergencyAccesses(encKey[0]);
await this.updateEmergencyAccesses(newUserKey);
await this.updateAllResetPasswordKeys(encKey[0], masterPasswordHash);
await this.updateAllResetPasswordKeys(newUserKey, masterPasswordHash);
}
private async updateEmergencyAccesses(encKey: SymmetricCryptoKey) {
@ -285,7 +296,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
}
}
private async updateAllResetPasswordKeys(encKey: SymmetricCryptoKey, masterPasswordHash: string) {
private async updateAllResetPasswordKeys(userKey: UserKey, masterPasswordHash: string) {
const orgs = await this.organizationService.getAll();
for (const org of orgs) {
@ -299,7 +310,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
const publicKey = Utils.fromB64ToArray(response?.publicKey);
// Re-enroll - encrypt user's encKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey);
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
// Create/Execute request
const request = new OrganizationUserResetPasswordEnrollmentRequest();

View File

@ -16,7 +16,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { DialogService } from "@bitwarden/components";
@ -91,9 +94,9 @@ export class EmergencyAccessTakeoverComponent
);
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
const oldUserKey = new SymmetricCryptoKey(oldKeyBuffer) as UserKey;
if (oldEncKey == null) {
if (oldUserKey == null) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
@ -102,7 +105,7 @@ export class EmergencyAccessTakeoverComponent
return;
}
const key = await this.cryptoService.makeKey(
const masterKey = await this.cryptoService.makeMasterKey(
this.masterPassword,
this.email,
takeoverResponse.kdf,
@ -112,12 +115,12 @@ export class EmergencyAccessTakeoverComponent
takeoverResponse.kdfParallelism
)
);
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
const masterKeyHash = await this.cryptoService.hashMasterKey(this.masterPassword, masterKey);
const encKey = await this.cryptoService.remakeEncKey(key, oldEncKey);
const encKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey, oldUserKey);
const request = new EmergencyAccessPasswordRequest();
request.newMasterPasswordHash = masterPasswordHash;
request.newMasterPasswordHash = masterKeyHash;
request.key = encKey[1].encryptedString;
this.apiService.postEmergencyAccessPassword(this.emergencyAccessId, request);

View File

@ -5,7 +5,10 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EmergencyAccessViewResponse } from "@bitwarden/common/auth/models/response/emergency-access.response";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
SymmetricCryptoKey,
UserKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@ -87,13 +90,13 @@ export class EmergencyAccessViewComponent implements OnInit {
const decCiphers: CipherView[] = [];
const oldKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
const oldEncKey = new SymmetricCryptoKey(oldKeyBuffer);
const oldUserKey = new SymmetricCryptoKey(oldKeyBuffer) as UserKey;
const promises: any[] = [];
ciphers.forEach((cipherResponse) => {
const cipherData = new CipherData(cipherResponse);
const cipher = new Cipher(cipherData);
promises.push(cipher.decrypt(oldEncKey).then((c) => decCiphers.push(c)));
promises.push(cipher.decrypt(oldUserKey).then((c) => decCiphers.push(c)));
});
await Promise.all(promises);

View File

@ -300,9 +300,12 @@ export class EmergencyAccessComponent implements OnInit {
}
}
// Encrypt the master password hash using the grantees public key, and send it to bitwarden for escrow.
// Encrypt the user key with the grantees public key, and send it to bitwarden for escrow.
private async doConfirmation(details: EmergencyAccessGranteeDetailsResponse) {
const encKey = await this.cryptoService.getEncKey();
const userKey = await this.cryptoService.getUserKey();
if (!userKey) {
throw new Error("No user key found");
}
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
@ -315,7 +318,7 @@ export class EmergencyAccessComponent implements OnInit {
// Ignore errors since it's just a debug message
}
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey);
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
const request = new EmergencyAccessConfirmRequest();
request.key = encryptedKey.encryptedString;
await this.apiService.postEmergencyAccessConfirm(details.id, request);

View File

@ -1,6 +1,5 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-body">
<p>{{ "twoStepLoginAuthDesc" | i18n }}</p>
<app-user-verification [(ngModel)]="secret" ngDefaultControl name="secret">
</app-user-verification>
</div>

View File

@ -1,4 +1,4 @@
<ng-container *ngIf="!usesKeyConnector">
<ng-container *ngIf="hasMasterPassword">
<bit-form-field disableMargin>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
@ -14,7 +14,7 @@
<bit-hint>{{ "confirmIdentity" | i18n }}</bit-hint>
</bit-form-field>
</ng-container>
<ng-container *ngIf="usesKeyConnector">
<ng-container *ngIf="!hasMasterPassword">
<div class="tw-mb-6">
<label class="tw-block">{{ "sendVerificationCode" | i18n }}</label>
<button type="button" bitButton buttonType="secondary" [bitAction]="requestOTP" appAutofocus>

View File

@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { HttpStatusCode } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -39,7 +40,8 @@ export class SsoComponent extends BaseSsoComponent {
logService: LogService,
private orgDomainApiService: OrgDomainApiServiceAbstraction,
private loginService: LoginService,
private validationService: ValidationService
private validationService: ValidationService,
configService: ConfigServiceAbstraction
) {
super(
authService,
@ -52,7 +54,8 @@ export class SsoComponent extends BaseSsoComponent {
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
logService,
configService
);
this.redirectUri = window.location.origin + "/sso-connector.html";
this.clientId = "web";

Some files were not shown because too many files have changed in this diff Show More