1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

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
249 changed files with 10155 additions and 2558 deletions

View File

@@ -200,6 +200,7 @@ export abstract class ApiService {
postConvertToKeyConnector: () => Promise<void>;
//passwordless
postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
postAdminAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
@@ -523,7 +524,7 @@ export abstract class ApiService {
) => Promise<void>;
postResendSponsorshipOffer: (sponsoringOrgId: string) => Promise<void>;
getUserKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
getMasterKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
postUserKeyToKeyConnector: (
keyConnectorUrl: string,
request: KeyConnectorUserKeyRequest

View File

@@ -1,8 +0,0 @@
import { DeviceKey } from "../platform/models/domain/symmetric-crypto-key";
import { DeviceResponse } from "./devices/responses/device.response";
export abstract class DeviceCryptoServiceAbstraction {
trustDevice: () => Promise<DeviceResponse>;
getDeviceKey: () => Promise<DeviceKey>;
}

View File

@@ -1,14 +0,0 @@
import { DeviceResponse } from "./responses/device.response";
export abstract class DevicesApiServiceAbstraction {
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
updateTrustedDeviceKeys: (
deviceIdentifier: string,
devicePublicKeyEncryptedUserSymKey: string,
userSymKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
) => Promise<DeviceResponse>;
}

View File

@@ -0,0 +1,15 @@
import { Observable } from "rxjs";
import { DeviceView } from "./views/device.view";
export abstract class DevicesServiceAbstraction {
getDevices$: () => Observable<Array<DeviceView>>;
getDeviceByIdentifier$: (deviceIdentifier: string) => Observable<DeviceView>;
isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable<boolean>;
updateTrustedDeviceKeys$: (
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
) => Observable<DeviceView>;
}

View File

@@ -3,23 +3,20 @@ import { BaseResponse } from "../../../models/response/base.response";
export class DeviceResponse extends BaseResponse {
id: string;
name: number;
userId: string;
name: string;
identifier: string;
type: DeviceType;
creationDate: string;
encryptedUserKey: string;
encryptedPublicKey: string;
encryptedPrivateKey: string;
revisionDate: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.userId = this.getResponseProperty("UserId");
this.name = this.getResponseProperty("Name");
this.identifier = this.getResponseProperty("Identifier");
this.type = this.getResponseProperty("Type");
this.creationDate = this.getResponseProperty("CreationDate");
this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
this.encryptedPrivateKey = this.getResponseProperty("EncryptedPrivateKey");
this.revisionDate = this.getResponseProperty("RevisionDate");
}
}

View File

@@ -0,0 +1,17 @@
import { DeviceType } from "../../../enums";
import { View } from "../../../models/view/view";
import { DeviceResponse } from "../responses/device.response";
export class DeviceView implements View {
id: string;
userId: string;
name: string;
identifier: string;
type: DeviceType;
creationDate: string;
revisionDate: string;
constructor(deviceResponse: DeviceResponse) {
Object.assign(this, deviceResponse);
}
}

View File

@@ -0,0 +1,56 @@
import { Observable } from "rxjs";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { PinLockType } from "../../services/vault-timeout/vault-timeout-settings.service";
export abstract class VaultTimeoutSettingsService {
/**
* Set the vault timeout options for the user
* @param vaultTimeout The vault timeout in minutes
* @param vaultTimeoutAction The vault timeout action
* @param userId The user id to set. If not provided, the current user is used
*/
setVaultTimeoutOptions: (
vaultTimeout: number,
vaultTimeoutAction: VaultTimeoutAction
) => Promise<void>;
/**
* Get the available vault timeout actions for the current user
*
* **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes
* @param userId The user id to check. If not provided, the current user is used
*/
availableVaultTimeoutActions$: (userId?: string) => Observable<VaultTimeoutAction[]>;
/**
* Get the current vault timeout action for the user. This is not the same as the current state, it is
* calculated based on the current state, the user's policy, and the user's available unlock methods.
*/
getVaultTimeout: (userId?: string) => Promise<number>;
/**
* Observe the vault timeout action for the user. This is calculated based on users preferred lock action saved in the state,
* the user's policy, and the user's available unlock methods.
*
* **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes
* @param userId The user id to check. If not provided, the current user is used
*/
vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>;
/**
* Has the user enabled unlock with Pin.
* @param userId The user id to check. If not provided, the current user is used
* @returns PinLockType
*/
isPinLockSet: (userId?: string) => Promise<PinLockType>;
/**
* Has the user enabled unlock with Biometric.
* @param userId The user id to check. If not provided, the current user is used
* @returns boolean true if biometric lock is set
*/
isBiometricLockSet: (userId?: string) => Promise<boolean>;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -1,13 +0,0 @@
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
export abstract class VaultTimeoutSettingsService {
setVaultTimeoutOptions: (
vaultTimeout: number,
vaultTimeoutAction: VaultTimeoutAction
) => Promise<void>;
getVaultTimeout: (userId?: string) => Promise<number>;
getVaultTimeoutAction: (userId?: string) => Promise<VaultTimeoutAction>;
isPinLockSet: () => Promise<[boolean, boolean]>;
isBiometricLockSet: () => Promise<boolean>;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -0,0 +1,24 @@
import { UserKey, MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestResponse } from "../models/response/auth-request.response";
export abstract class AuthRequestCryptoServiceAbstraction {
setUserKeyAfterDecryptingSharedUserKey: (
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer
) => Promise<void>;
setKeysAfterDecryptingSharedMasterKeyAndHash: (
authReqResponse: AuthRequestResponse,
authReqPrivateKey: ArrayBuffer
) => Promise<void>;
decryptPubKeyEncryptedUserKey: (
pubKeyEncryptedUserKey: string,
privateKey: ArrayBuffer
) => Promise<UserKey>;
decryptPubKeyEncryptedMasterKeyAndHash: (
pubKeyEncryptedMasterKey: string,
pubKeyEncryptedMasterKeyHash: string,
privateKey: ArrayBuffer
) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>;
}

View File

@@ -1,7 +1,7 @@
import { Observable } from "rxjs";
import { AuthRequestPushNotification } from "../../models/response/notification.response";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { AuthenticationStatus } from "../enums/authentication-status";
import { AuthResult } from "../models/domain/auth-result";
import {
@@ -32,7 +32,7 @@ export abstract class AuthService {
captchaResponse: string
) => Promise<AuthResult>;
logOut: (callback: () => void) => void;
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
authingWithUserApiKey: () => boolean;
authingWithSso: () => boolean;
authingWithPassword: () => boolean;

View File

@@ -0,0 +1,25 @@
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { EncString } from "../../platform/models/domain/enc-string";
import { DeviceKey, UserKey } from "../../platform/models/domain/symmetric-crypto-key";
export abstract class DeviceTrustCryptoServiceAbstraction {
/**
* @description Retrieves the users choice to trust the device which can only happen after decryption
* Note: this value should only be used once and then reset
*/
getShouldTrustDevice: () => Promise<boolean | null>;
setShouldTrustDevice: (value: boolean) => Promise<void>;
trustDeviceIfRequired: () => Promise<void>;
trustDevice: () => Promise<DeviceResponse>;
getDeviceKey: () => Promise<DeviceKey>;
decryptUserKeyWithDeviceKey: (
encryptedDevicePrivateKey: EncString,
encryptedUserKey: EncString,
deviceKey?: DeviceKey
) => Promise<UserKey | null>;
rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>;
supportsDeviceTrust: () => Promise<boolean>;
}

View File

@@ -0,0 +1,27 @@
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { ListResponse } from "../../models/response/list.response";
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
export abstract class DevicesApiServiceAbstraction {
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
getDevices: () => Promise<ListResponse<DeviceResponse>>;
updateTrustedDeviceKeys: (
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
) => Promise<DeviceResponse>;
updateTrust: (updateDevicesTrustRequestModel: UpdateDevicesTrustRequest) => Promise<void>;
getDeviceKeys: (
deviceIdentifier: string,
secretVerificationRequest: SecretVerificationRequest
) => Promise<ProtectedDeviceResponse>;
}

View File

@@ -2,7 +2,7 @@ import { Organization } from "../../admin-console/models/domain/organization";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
export abstract class KeyConnectorService {
getAndSetKey: (url?: string) => Promise<void>;
setMasterKeyFromUrl: (url?: string) => Promise<void>;
getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>;

View File

@@ -0,0 +1,26 @@
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
export abstract class PasswordResetEnrollmentServiceAbstraction {
/*
* Checks the user's enrollment status and enrolls them if required
*/
abstract enrollIfRequired(organizationSsoIdentifier: string): Promise<void>;
/**
* Enroll current user in password reset
* @param organizationId - Organization in which to enroll the user
* @returns Promise that resolves when the user is enrolled
* @throws Error if the action fails
*/
abstract enroll(organizationId: string): Promise<void>;
/**
* Enroll user in password reset
* @param organizationId - Organization in which to enroll the user
* @param userId - User to enroll
* @param userKey - User's symmetric key
* @returns Promise that resolves when the user is enrolled
* @throws Error if the action fails
*/
abstract enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
}

View File

@@ -9,4 +9,16 @@ export abstract class UserVerificationService {
) => Promise<T>;
verifyUser: (verification: Verification) => Promise<boolean>;
requestOTP: () => Promise<void>;
/**
* Check if user has master password or only uses passwordless technologies to log in
* @param userId The user id to check. If not provided, the current user is used
* @returns True if the user has a master password
*/
hasMasterPassword: (userId?: string) => Promise<boolean>;
/**
* Check if the user has a master password and has used it during their current session
* @param userId The user id to check. If not provided, the current user id used
* @returns True if the user has a master password and has used it in the current session
*/
hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise<boolean>;
}

View File

@@ -1,4 +1,5 @@
export enum AuthRequestType {
AuthenticateAndUnlock = 0,
Unlock = 1,
AdminApproval = 2,
}

View File

@@ -9,12 +9,25 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { Account, AccountProfile, AccountTokens } from "../../platform/models/domain/account";
import {
Account,
AccountDecryptionOptions,
AccountKeys,
AccountProfile,
AccountTokens,
} from "../../platform/models/domain/account";
import { EncString } from "../../platform/models/domain/enc-string";
import {
DeviceKey,
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import {
PasswordStrengthService,
PasswordStrengthServiceAbstraction,
} from "../../tools/password-strength";
import { CsprngArray } from "../../types/csprng";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -28,6 +41,10 @@ import { IdentityCaptchaResponse } from "../models/response/identity-captcha.res
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
import {
IUserDecryptionOptionsServerResponse,
UserDecryptionOptionsResponse,
} from "../models/response/user-decryption-options/user-decryption-options.response";
import { PasswordLogInStrategy } from "./password-login.strategy";
@@ -37,7 +54,7 @@ const masterPassword = "password";
const deviceId = Utils.newGuid();
const accessToken = "ACCESS_TOKEN";
const refreshToken = "REFRESH_TOKEN";
const encKey = "ENC_KEY";
const userKey = "USER_KEY";
const privateKey = "PRIVATE_KEY";
const captchaSiteKey = "CAPTCHA_SITE_KEY";
const kdf = 0;
@@ -45,6 +62,13 @@ const kdfIterations = 10000;
const userId = Utils.newGuid();
const masterPasswordHash = "MASTER_PASSWORD_HASH";
const name = "NAME";
const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = {
HasMasterPassword: true,
};
const userDecryptionOptions = new UserDecryptionOptionsResponse(
defaultUserDecryptionOptionsServerResponse
);
const acctDecryptionOptions = AccountDecryptionOptions.fromResponse(userDecryptionOptions);
const decodedToken = {
sub: userId,
@@ -58,13 +82,14 @@ const twoFactorToken = "TWO_FACTOR_TOKEN";
const twoFactorRemember = true;
export function identityTokenResponseFactory(
masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null
masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null,
userDecryptionOptions: IUserDecryptionOptionsServerResponse = null
) {
return new IdentityTokenResponse({
ForcePasswordReset: false,
Kdf: kdf,
KdfIterations: kdfIterations,
Key: encKey,
Key: userKey,
PrivateKey: privateKey,
ResetMasterPassword: false,
access_token: accessToken,
@@ -73,9 +98,11 @@ export function identityTokenResponseFactory(
scope: "api offline_access",
token_type: "Bearer",
MasterPasswordPolicy: masterPasswordPolicyResponse,
UserDecryptionOptions: userDecryptionOptions || defaultUserDecryptionOptionsServerResponse,
});
}
// TODO: add tests for latest changes to base class for TDE
describe("LogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
@@ -129,8 +156,23 @@ describe("LogInStrategy", () => {
});
describe("base class", () => {
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
const userKeyBytesLength = 64;
const masterKeyBytesLength = 64;
let userKey: UserKey;
let masterKey: MasterKey;
beforeEach(() => {
userKey = new SymmetricCryptoKey(
new Uint8Array(userKeyBytesLength).buffer as CsprngArray
) as UserKey;
masterKey = new SymmetricCryptoKey(
new Uint8Array(masterKeyBytesLength).buffer as CsprngArray
) as MasterKey;
});
it("sets the local environment after a successful login with master password", async () => {
const idTokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
await passwordLogInStrategy.logIn(credentials);
@@ -154,13 +196,36 @@ describe("LogInStrategy", () => {
refreshToken: refreshToken,
},
},
keys: new AccountKeys(),
decryptionOptions: acctDecryptionOptions,
})
);
expect(cryptoService.setEncKey).toHaveBeenCalledWith(encKey);
expect(cryptoService.setEncPrivateKey).toHaveBeenCalledWith(privateKey);
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
});
it("persists a device key for trusted device encryption when it exists on login", async () => {
// Arrange
const idTokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
const deviceKey = new SymmetricCryptoKey(
new Uint8Array(userKeyBytesLength).buffer as CsprngArray
) as DeviceKey;
stateService.getDeviceKey.mockResolvedValue(deviceKey);
const accountKeys = new AccountKeys();
accountKeys.deviceKey = deviceKey;
// Act
await passwordLogInStrategy.logIn(credentials);
// Assert
expect(stateService.addAccount).toHaveBeenCalledWith(
expect.objectContaining({ keys: accountKeys })
);
});
it("builds AuthResult", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.forcePasswordReset = true;
@@ -187,6 +252,8 @@ describe("LogInStrategy", () => {
});
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
const result = await passwordLogInStrategy.logIn(credentials);
@@ -204,13 +271,15 @@ describe("LogInStrategy", () => {
cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await passwordLogInStrategy.logIn(credentials);
// User key must be set before the new RSA keypair is generated, otherwise we can't decrypt the EncKey
expect(cryptoService.setKey).toHaveBeenCalled();
// User symmetric key must be set before the new RSA keypair is generated
expect(cryptoService.setUserKey).toHaveBeenCalled();
expect(cryptoService.makeKeyPair).toHaveBeenCalled();
expect(cryptoService.setKey.mock.invocationCallOrder[0]).toBeLessThan(
expect(cryptoService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan(
cryptoService.makeKeyPair.mock.invocationCallOrder[0]
);

View File

@@ -6,7 +6,13 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Account, AccountProfile, AccountTokens } from "../../platform/models/domain/account";
import {
Account,
AccountDecryptionOptions,
AccountKeys,
AccountProfile,
AccountTokens,
} from "../../platform/models/domain/account";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
@@ -53,9 +59,6 @@ export abstract class LogInStrategy {
| PasswordlessLogInCredentials
): Promise<AuthResult>;
// The user key comes from different sources depending on the login strategy
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string = null
@@ -101,12 +104,28 @@ export abstract class LogInStrategy {
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
// Must persist existing device key if it exists for trusted device decryption to work
// However, we must provide a user id so that the device key can be retrieved
// as the state service won't have an active account at this point in time
// even though the data exists in local storage.
const userId = accountInformation.sub;
const deviceKey = await this.stateService.getDeviceKey({ userId });
const accountKeys = new AccountKeys();
if (deviceKey) {
accountKeys.deviceKey = deviceKey;
}
// If you don't persist existing admin auth requests on login, they will get deleted.
const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId });
await this.stateService.addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: accountInformation.sub,
userId,
name: accountInformation.name,
email: accountInformation.email,
hasPremiumPersonally: accountInformation.premium,
@@ -123,6 +142,11 @@ export abstract class LogInStrategy {
refreshToken: tokenResponse.refreshToken,
},
},
keys: accountKeys,
decryptionOptions: AccountDecryptionOptions.fromResponse(
tokenResponse.userDecryptionOptions
),
adminAuthRequest: adminAuthRequest?.toJSON(),
})
);
}
@@ -135,28 +159,41 @@ export abstract class LogInStrategy {
result.forcePasswordReset = ForceResetPasswordReason.AdminForcePasswordReset;
}
// Must come before setting keys, user key needs email to update additional keys
await this.saveAccountInformation(response);
if (response.twoFactorToken != null) {
await this.tokenService.setTwoFactorToken(response);
}
await this.setMasterKey(response);
await this.setUserKey(response);
// Must come after the user Key is set, otherwise createKeyPairForOldAccount will fail
const newSsoUser = response.key == null;
if (!newSsoUser) {
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
await this.setPrivateKey(response);
this.messagingService.send("loggedIn");
return result;
}
// The keys comes from different sources depending on the login strategy
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
protected async createKeyPairForOldAccount() {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
return privateKey.encryptedString;
} catch (e) {
this.logService.error(e);
}
}
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
const result = new AuthResult();
result.twoFactorProviders = response.twoFactorProviders2;
@@ -173,14 +210,4 @@ export abstract class LogInStrategy {
result.captchaSiteKey = response.siteKey;
return result;
}
private async createKeyPairForOldAccount() {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
return privateKey.encryptedString;
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -10,17 +10,23 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import {
PasswordStrengthService,
PasswordStrengthServiceAbstraction,
} from "../../tools/password-strength";
import { CsprngArray } from "../../types/csprng";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
@@ -31,11 +37,11 @@ const email = "hello@world.com";
const masterPassword = "password";
const hashedPassword = "HASHED_PASSWORD";
const localHashedPassword = "LOCAL_HASHED_PASSWORD";
const preloginKey = new SymmetricCryptoKey(
const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
)
);
) as MasterKey;
const deviceId = Utils.newGuid();
const masterPasswordPolicy = new MasterPasswordPolicyResponse({
EnforceOnLogin: true,
@@ -58,6 +64,7 @@ describe("PasswordLogInStrategy", () => {
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
let tokenResponse: IdentityTokenResponse;
beforeEach(async () => {
cryptoService = mock<CryptoService>();
@@ -76,12 +83,12 @@ describe("PasswordLogInStrategy", () => {
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
authService.makePreloginKey.mockResolvedValue(preloginKey);
authService.makePreloginKey.mockResolvedValue(masterKey);
cryptoService.hashPassword
cryptoService.hashMasterKey
.calledWith(masterPassword, expect.anything(), undefined)
.mockResolvedValue(hashedPassword);
cryptoService.hashPassword
cryptoService.hashMasterKey
.calledWith(masterPassword, expect.anything(), HashPurpose.LocalAuthorization)
.mockResolvedValue(localHashedPassword);
@@ -102,10 +109,9 @@ describe("PasswordLogInStrategy", () => {
authService
);
credentials = new PasswordLogInCredentials(email, masterPassword);
tokenResponse = identityTokenResponseFactory(masterPasswordPolicy);
apiService.postIdentityToken.mockResolvedValue(
identityTokenResponseFactory(masterPasswordPolicy)
);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
});
it("sends master password credentials to the server", async () => {
@@ -127,15 +133,23 @@ describe("PasswordLogInStrategy", () => {
);
});
it("sets the local environment after a successful login", async () => {
it("sets keys after a successful authentication", async () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await passwordLogInStrategy.logIn(credentials);
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey);
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword);
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(localHashedPassword);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
});
it("does not force the user to update their master password when there are no requirements", async () => {
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory(null));
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory());
const result = await passwordLogInStrategy.logIn(credentials);

View File

@@ -8,7 +8,7 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
@@ -35,8 +35,8 @@ export class PasswordLogInStrategy extends LogInStrategy {
tokenRequest: PasswordTokenRequest;
private localHashedPassword: string;
private key: SymmetricCryptoKey;
private localMasterKeyHash: string;
private masterKey: MasterKey;
/**
* Options to track if the user needs to update their password due to a password that does not meet an organization's
@@ -71,12 +71,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
);
}
async setUserKey() {
await this.cryptoService.setKey(this.key);
await this.cryptoService.setKeyHash(this.localHashedPassword);
}
async logInTwoFactor(
override async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string
): Promise<AuthResult> {
@@ -96,28 +91,29 @@ export class PasswordLogInStrategy extends LogInStrategy {
return result;
}
async logIn(credentials: PasswordLogInCredentials) {
override async logIn(credentials: PasswordLogInCredentials) {
const { email, masterPassword, captchaToken, twoFactor } = credentials;
this.key = await this.authService.makePreloginKey(masterPassword, email);
this.masterKey = await this.authService.makePreloginKey(masterPassword, email);
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
this.localHashedPassword = await this.cryptoService.hashPassword(
this.localMasterKeyHash = await this.cryptoService.hashMasterKey(
masterPassword,
this.key,
this.masterKey,
HashPurpose.LocalAuthorization
);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key);
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, this.masterKey);
this.tokenRequest = new PasswordTokenRequest(
email,
hashedPassword,
masterKeyHash,
captchaToken,
await this.buildTwoFactor(twoFactor),
await this.buildDeviceRequest()
);
const [authResult, identityResponse] = await this.startLogIn();
const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
@@ -145,6 +141,27 @@ export class PasswordLogInStrategy extends LogInStrategy {
return authResult;
}
protected override async setMasterKey(response: IdentityTokenResponse) {
await this.cryptoService.setMasterKey(this.masterKey);
await this.cryptoService.setMasterKeyHash(this.localMasterKeyHash);
}
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
const masterKey = await this.cryptoService.getMasterKey();
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
): MasterPasswordPolicyOptions {

View File

@@ -0,0 +1,138 @@
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { identityTokenResponseFactory } from "./login.strategy.spec";
import { PasswordlessLogInStrategy } from "./passwordless-login.strategy";
describe("PasswordlessLogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
let tokenService: MockProxy<TokenService>;
let appIdService: MockProxy<AppIdService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let messagingService: MockProxy<MessagingService>;
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let passwordlessLoginStrategy: PasswordlessLogInStrategy;
let credentials: PasswordlessLogInCredentials;
let tokenResponse: IdentityTokenResponse;
const deviceId = Utils.newGuid();
const email = "EMAIL";
const accessCode = "ACCESS_CODE";
const authRequestId = "AUTH_REQUEST_ID";
const decMasterKey = new SymmetricCryptoKey(
new Uint8Array(64).buffer as CsprngArray
) as MasterKey;
const decUserKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
const decMasterKeyHash = "LOCAL_PASSWORD_HASH";
beforeEach(async () => {
cryptoService = mock<CryptoService>();
apiService = mock<ApiService>();
tokenService = mock<TokenService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
messagingService = mock<MessagingService>();
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
passwordlessLoginStrategy = new PasswordlessLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
deviceTrustCryptoService
);
tokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
});
it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => {
credentials = new PasswordlessLogInCredentials(
email,
accessCode,
authRequestId,
null,
decMasterKey,
decMasterKeyHash
);
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await passwordlessLoginStrategy.logIn(credentials);
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled();
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
});
it("sets keys after a successful authentication when only userKey provided in login credentials", async () => {
// Initialize credentials with only userKey
credentials = new PasswordlessLogInCredentials(
email,
accessCode,
authRequestId,
decUserKey, // Pass userKey
null, // No masterKey
null // No masterKeyHash
);
// Call logIn
await passwordlessLoginStrategy.logIn(credentials);
// setMasterKey and setMasterKeyHash should not be called
expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
expect(cryptoService.setMasterKeyHash).not.toHaveBeenCalled();
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
// trustDeviceIfRequired should be called
expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled();
});
});

View File

@@ -5,13 +5,14 @@ import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { AuthService } from "../abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { AuthResult } from "../models/domain/auth-result";
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { LogInStrategy } from "./login.strategy";
@@ -41,7 +42,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private authService: AuthService
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
) {
super(
cryptoService,
@@ -56,20 +57,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
);
}
async setUserKey() {
await this.cryptoService.setKey(this.passwordlessCredentials.decKey);
await this.cryptoService.setKeyHash(this.passwordlessCredentials.localPasswordHash);
}
async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
}
async logIn(credentials: PasswordlessLogInCredentials) {
override async logIn(credentials: PasswordlessLogInCredentials) {
this.passwordlessCredentials = credentials;
this.tokenRequest = new PasswordTokenRequest(
@@ -84,4 +72,52 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
const [authResult] = await this.startLogIn();
return authResult;
}
override async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
}
protected override async setMasterKey(response: IdentityTokenResponse) {
if (
this.passwordlessCredentials.decryptedMasterKey &&
this.passwordlessCredentials.decryptedMasterKeyHash
) {
await this.cryptoService.setMasterKey(this.passwordlessCredentials.decryptedMasterKey);
await this.cryptoService.setMasterKeyHash(
this.passwordlessCredentials.decryptedMasterKeyHash
);
}
}
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
// User now may or may not have a master password
// but set the master key encrypted user key if it exists regardless
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
if (this.passwordlessCredentials.decryptedUserKey) {
await this.cryptoService.setUserKey(this.passwordlessCredentials.decryptedUserKey);
} else {
await this.trySetUserKeyWithMasterKey();
// Establish trust if required after setting user key
await this.deviceTrustCryptoService.trustDeviceIfRequired();
}
}
private async trySetUserKeyWithMasterKey(): Promise<void> {
const masterKey = await this.cryptoService.getMasterKey();
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
}

View File

@@ -3,19 +3,34 @@ import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import {
DeviceKey,
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { SsoLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IUserDecryptionOptionsServerResponse } from "../models/response/user-decryption-options/user-decryption-options.response";
import { identityTokenResponseFactory } from "./login.strategy.spec";
import { SsoLogInStrategy } from "./sso-login.strategy";
// TODO: Add tests for new trySetUserKeyWithApprovedAdminRequestIfExists logic
// https://bitwarden.atlassian.net/browse/PM-3339
describe("SsoLogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
let apiService: MockProxy<ApiService>;
@@ -27,6 +42,9 @@ describe("SsoLogInStrategy", () => {
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestCryptoService: MockProxy<AuthRequestCryptoServiceAbstraction>;
let i18nService: MockProxy<I18nService>;
let ssoLogInStrategy: SsoLogInStrategy;
let credentials: SsoLogInCredentials;
@@ -50,6 +68,9 @@ describe("SsoLogInStrategy", () => {
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
keyConnectorService = mock<KeyConnectorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestCryptoService = mock<AuthRequestCryptoServiceAbstraction>();
i18nService = mock<I18nService>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId);
@@ -65,7 +86,10 @@ describe("SsoLogInStrategy", () => {
logService,
stateService,
twoFactorService,
keyConnectorService
keyConnectorService,
deviceTrustCryptoService,
authRequestCryptoService,
i18nService
);
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
});
@@ -98,33 +122,194 @@ describe("SsoLogInStrategy", () => {
await ssoLogInStrategy.logIn(credentials);
expect(cryptoService.setEncPrivateKey).not.toHaveBeenCalled();
expect(cryptoService.setEncKey).not.toHaveBeenCalled();
expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
expect(cryptoService.setPrivateKey).not.toHaveBeenCalled();
});
it("gets and sets KeyConnector key for enrolled user", async () => {
it("sets master key encrypted user key for existing SSO users", async () => {
// Arrange
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
// Act
await ssoLogInStrategy.logIn(credentials);
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
// Assert
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
});
it("converts new SSO user to Key Connector on first login", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
tokenResponse.key = null;
describe("Trusted Device Decryption", () => {
const deviceKeyBytesLength = 64;
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
const mockDeviceKey: DeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
const userKeyBytesLength = 64;
const mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength).buffer as CsprngArray;
const mockUserKey: UserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
await ssoLogInStrategy.logIn(credentials);
const mockEncDevicePrivateKey =
"2.eh465OrUcluL9UpnCOUTAg==|2HXNXwrLwAjUfZ/U75c92rZEltt1eHxjMkp/ADAmx346oT1+GaQvaL1QIV/9Om0T72m8AnlO92iUfWdhbA/ifHZ+lhFoUVeyw1M88CMzktbVcq42rFoK7SGHSAGdTL3ccUWKI8yCCQJhpt2X6a/5+T7ey5k2CqvylKyOtkiCnVeLmYqETn5BM9Rl3tEgJW1yDLuSJ+L+Qh9xnk/Z3zJUV5HAs+YwjKwuSNrd00SXjDyx8rBEstD9MKI+lrk7to/q90vqKqCucAj/dzUpVtHe88al2AAlBVwQ13HUPdNFOyti6niUgCAWx+DzRqlhkFvl/z/rtxtQsyqq/3Eh/EL54ylxKzAya0ev9EaIOm/dD1aBmI58p4Bs0eMOCIKJjtw+Cmdql+RhCtKtumgFShqyXv+LfD/FgUsdTVNExk3YNhgwPR4jOaMa/j9LCrBMCLKxdAhQyBe7T3qoX1fBBirvY6t77ifMu1YEQ6DfmFphVSwDH5C9xGeTSh5IELSf0tGVtlWUe9RffDDzccD0L1lR8U+dqzoSTYCuXvhEhQptdIW6fpH/47u0M5MiI97/d35A7Et2I1gjHp7WF3qsY20ellBueu7ZL5P1BmqPXl58yaBBXJaCutYHDfIucspqdZmfBGEbdRT4wmuZRON0J8zLmUejM0VR/2MOmpfyYQXnJhTfrvnZ1bOg1aMhUxJ2vhDNPXUFm5b+vwsho4GEvcLAKq9WwbvOJ/sK7sEVfTfEO2IG+0X6wkWm7RpR6Wq9FGKSrv2PSjMAYnb+z3ETeWiaaiD+tVFxa2AaqsbOuX092/86GySpHES7cFWhQ/YMOgj6egUi8mEC0CqMXYsx0TTJDsn16oP+XB3a2WoRqzE0YBozp2aMXxhVf/jMZ03BmEmRQu5B+Sq1gMEZwtIfJ+srkZLMYlLjvVw92FRoFy+N6ytPiyf6RMHMUnJ3vEZSBogaElYoQAtFJ5kK811CUzb78zEHH8xWtPrCZn9zZfvf/zaWxo7fpV8VwAwUeHXHcQMraZum5QeO+5tLRUYrLm85JNelGfmUA3BjfNyFbfb32PhkWWd0CbDaPME48uIriVK32pNEtvtR/+I/f3YgA/jP9kSlDvbzG/OAg/AFBIpNwKUzsu4+va8mI+O5FDufw5D74WwdGJ9DeyEb2CHtWMR1VwtFKL0ZZsqltNf8EkBeJ5RtTNtAMM8ie4dDZaKC96ymQHKrdB4hjkAr0F1XFsU4XdOa9Nbkdcm/7KoNc6bE6oJtG9lqE8h+1CysfcbfJ7am+hvDFzT0IPmp3GDSMAk+e6xySgFQw0C/SZ7LQsxPa1s6hc+BOtTn0oClZnU7Mowxv+z+xURJj4Yp3Cy6tAoia1jEQSs6lSMNKPf9bi3xFKtPl4143hwhpvTAzJUcski9OVGd7Du+VyxwIrvLqp5Ct/oNrESVJpf1EDCs9xT1EW+PiSkRmHXoZ1t5MOLFEiMAZL2+bNe3A2661oJeMtps8zrfCVc251OUE1WvqWePlTOs5TDVqdwDH88J6rHLsbaf33Mxh5DP8gMfZQxE44Nsp6H0/Szfkss5UmFwBEpHjl1GJMWDnB3u2d+l1CSkLoB6C+diAUlY6wL/VwJBeMPHZTf6amQIS2B/lo/CnvV/E3k=|uuoY4b7xwMYBNIZi85KBsaHmNqtJl5FrKxZI9ugeNwc=";
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
tokenResponse,
ssoOrgId
);
const mockEncUserKey =
"4.Xht6K9GA9jKcSNy4TaIvdj7f9+WsgQycs/HdkrJi33aC//roKkjf3UTGpdzFLxVP3WhyOVGyo9f2Jymf1MFPdpg7AuMnpGJlcrWLDbnPjOJo4x5gUwwBUmy3nFw6+wamyS1LRmrBPcv56yKpf80k5Q3hUrum8q9YS9m2I10vklX/TaB1YML0yo+K1feWUxg8vIx+vloxhUdkkysvcV5xU3R+AgYLrwvJS8TLL7Ug/P5HxinCaIroRrNe8xcv84vyVnzPFdXe0cfZ0cpcrm586LwfEXP2seeldO/bC51Uk/mudeSALJURPC64f5ch2cOvk48GOTapGnssCqr6ky5yFw==";
const userDecryptionOptsServerResponseWithTdeOption: IUserDecryptionOptionsServerResponse = {
HasMasterPassword: true,
TrustedDeviceOption: {
HasAdminApproval: true,
HasLoginApprovingDevice: true,
HasManageResetPasswordPermission: false,
EncryptedPrivateKey: mockEncDevicePrivateKey,
EncryptedUserKey: mockEncUserKey,
},
};
const mockIdTokenResponseWithModifiedTrustedDeviceOption = (key: string, value: any) => {
const userDecryptionOpts: IUserDecryptionOptionsServerResponse = {
...userDecryptionOptsServerResponseWithTdeOption,
TrustedDeviceOption: {
...userDecryptionOptsServerResponseWithTdeOption.TrustedDeviceOption,
[key]: value,
},
};
return identityTokenResponseFactory(null, userDecryptionOpts);
};
beforeEach(() => {
jest.clearAllMocks();
});
it("decrypts and sets user key when trusted device decryption option exists with valid device key and enc key data", async () => {
// Arrange
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
null,
userDecryptionOptsServerResponseWithTdeOption
);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey);
const cryptoSvcSetUserKeySpy = jest.spyOn(cryptoService, "setUserKey");
// Act
await ssoLogInStrategy.logIn(credentials);
// Assert
expect(deviceTrustCryptoService.getDeviceKey).toHaveBeenCalledTimes(1);
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1);
expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledWith(mockUserKey);
});
it("does not set the user key when deviceKey is missing", async () => {
// Arrange
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
null,
userDecryptionOptsServerResponseWithTdeOption
);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
// Set deviceKey to be null
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(null);
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey);
// Act
await ssoLogInStrategy.logIn(credentials);
// Assert
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
});
describe.each([
{
valueName: "encDevicePrivateKey",
},
{
valueName: "encUserKey",
},
])("given trusted device decryption option has missing encrypted key data", ({ valueName }) => {
it(`does not set the user key when ${valueName} is missing`, async () => {
// Arrange
const idTokenResponse = mockIdTokenResponseWithModifiedTrustedDeviceOption(valueName, null);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
// Act
await ssoLogInStrategy.logIn(credentials);
// Assert
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
});
});
it("does not set user key when decrypted user key is null", async () => {
// Arrange
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
null,
userDecryptionOptsServerResponseWithTdeOption
);
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
// Set userKey to be null
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(null);
// Act
await ssoLogInStrategy.logIn(credentials);
// Assert
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
});
});
describe("Key Connector", () => {
let tokenResponse: IdentityTokenResponse;
beforeEach(() => {
tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
});
it("gets and sets the master key if Key Connector is enabled", async () => {
const masterKey = new SymmetricCryptoKey(
new Uint8Array(64).buffer as CsprngArray
) as MasterKey;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
await ssoLogInStrategy.logIn(credentials);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
});
it("converts new SSO user to Key Connector on first login", async () => {
tokenResponse.key = null;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
tokenResponse,
ssoOrgId
);
});
it("decrypts and sets the user key if Key Connector is enabled", async () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
const masterKey = new SymmetricCryptoKey(
new Uint8Array(64).buffer as CsprngArray
) as MasterKey;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await ssoLogInStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
});
});
});

View File

@@ -1,10 +1,16 @@
import { ApiService } from "../../abstractions/api.service";
import { AuthRequestResponse } from "../../auth/models/response/auth-request.response";
import { HttpStatusCode } from "../../enums";
import { ErrorResponse } from "../../models/response/error.response";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -34,7 +40,10 @@ export class SsoLogInStrategy extends LogInStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private keyConnectorService: KeyConnectorService
private keyConnectorService: KeyConnectorService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authReqCryptoService: AuthRequestCryptoServiceAbstraction,
private i18nService: I18nService
) {
super(
cryptoService,
@@ -49,18 +58,6 @@ export class SsoLogInStrategy extends LogInStrategy {
);
}
async setUserKey(tokenResponse: IdentityTokenResponse) {
const newSsoUser = tokenResponse.key == null;
if (tokenResponse.keyConnectorUrl != null) {
if (!newSsoUser) {
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
} else {
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
}
}
}
async logIn(credentials: SsoLogInCredentials) {
this.orgId = credentials.orgId;
this.tokenRequest = new SsoTokenRequest(
@@ -78,4 +75,155 @@ export class SsoLogInStrategy extends LogInStrategy {
return ssoAuthResult;
}
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
// TODO: discuss how this is no longer true with TDE
// eventually well need to support migration of existing TDE users to Key Connector
const newSsoUser = tokenResponse.key == null;
if (tokenResponse.keyConnectorUrl != null) {
if (!newSsoUser) {
await this.keyConnectorService.setMasterKeyFromUrl(tokenResponse.keyConnectorUrl);
} else {
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
}
}
}
// TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request)
// so might be worth moving this logic to a common place (base login strategy or a separate service?)
protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise<void> {
const masterKeyEncryptedUserKey = tokenResponse.key;
// Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users
// on account creation and subsequent logins (confirmed or unconfirmed)
// but that is fine for TDE so we cannot return if it is undefined
if (masterKeyEncryptedUserKey) {
// set the master key encrypted user key if it exists
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
}
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
// Note: TDE and key connector are mutually exclusive
if (userDecryptionOptions?.trustedDeviceOption) {
await this.trySetUserKeyWithApprovedAdminRequestIfExists();
const hasUserKey = await this.cryptoService.hasUserKey();
// Only try to set user key with device key if admin approval request was not successful
if (!hasUserKey) {
await this.trySetUserKeyWithDeviceKey(tokenResponse);
}
} else if (
// TODO: remove tokenResponse.keyConnectorUrl when it's deprecated
masterKeyEncryptedUserKey != null &&
(tokenResponse.keyConnectorUrl || userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl)
) {
// Key connector enabled for user
await this.trySetUserKeyWithMasterKey();
}
// Note: In the traditional SSO flow with MP without key connector, the lock component
// is responsible for deriving master key from MP entry and then decrypting the user key
}
private async trySetUserKeyWithApprovedAdminRequestIfExists(): Promise<void> {
// At this point a user could have an admin auth request that has been approved
const adminAuthReqStorable = await this.stateService.getAdminAuthRequest();
if (!adminAuthReqStorable) {
return;
}
// Call server to see if admin auth request has been approved
let adminAuthReqResponse: AuthRequestResponse;
try {
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
// if we get a 404, it means the auth request has been deleted so clear it from storage
await this.stateService.setAdminAuthRequest(null);
}
// Always return on an error here as we don't want to block the user from logging in
return;
}
if (adminAuthReqResponse?.requestApproved) {
// if masterPasswordHash has a value, we will always receive authReqResponse.key
// as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
if (adminAuthReqResponse.masterPasswordHash) {
await this.authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
adminAuthReqResponse,
adminAuthReqStorable.privateKey
);
} else {
// if masterPasswordHash is null, we will always receive authReqResponse.key
// as authRequestPublicKey(userKey)
await this.authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
adminAuthReqResponse,
adminAuthReqStorable.privateKey
);
}
if (await this.cryptoService.hasUserKey()) {
// Now that we have a decrypted user key in memory, we can check if we
// need to establish trust on the current device
await this.deviceTrustCryptoService.trustDeviceIfRequired();
// if we successfully decrypted the user key, we can delete the admin auth request out of state
// TODO: eventually we post and clean up DB as well once consumed on client
await this.stateService.setAdminAuthRequest(null);
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
}
}
}
private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> {
const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption;
const deviceKey = await this.deviceTrustCryptoService.getDeviceKey();
const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey;
const encUserKey = trustedDeviceOption?.encryptedUserKey;
if (!deviceKey || !encDevicePrivateKey || !encUserKey) {
return;
}
const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
encDevicePrivateKey,
encUserKey,
deviceKey
);
if (userKey) {
await this.cryptoService.setUserKey(userKey);
}
}
private async trySetUserKeyWithMasterKey(): Promise<void> {
const masterKey = await this.cryptoService.getMasterKey();
if (!masterKey) {
throw new Error("Master key not found");
}
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise<void> {
const newSsoUser = tokenResponse.key == null;
if (!newSsoUser) {
await this.cryptoService.setPrivateKey(
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
}
}

View File

@@ -9,6 +9,12 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -101,7 +107,18 @@ describe("UserApiLogInStrategy", () => {
expect(stateService.addAccount).toHaveBeenCalled();
});
it("gets and sets the Key Connector key from environmentUrl", async () => {
it("sets the encrypted user key and private key from the identity token response", async () => {
const tokenResponse = identityTokenResponseFactory();
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
});
it("gets and sets the master key if Key Connector is enabled", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
@@ -110,6 +127,24 @@ describe("UserApiLogInStrategy", () => {
await apiLogInStrategy.logIn(credentials);
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
});
it("decrypts and sets the user key if Key Connector is enabled", async () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
});
});

View File

@@ -44,14 +44,7 @@ export class UserApiLogInStrategy extends LogInStrategy {
);
}
async setUserKey(tokenResponse: IdentityTokenResponse) {
if (tokenResponse.apiUseKeyConnector) {
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
}
}
async logIn(credentials: UserApiLogInCredentials) {
override async logIn(credentials: UserApiLogInCredentials) {
this.tokenRequest = new UserApiTokenRequest(
credentials.clientId,
credentials.clientSecret,
@@ -63,6 +56,31 @@ export class UserApiLogInStrategy extends LogInStrategy {
return authResult;
}
protected override async setMasterKey(response: IdentityTokenResponse) {
if (response.apiUseKeyConnector) {
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
}
}
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
if (response.apiUseKeyConnector) {
const masterKey = await this.cryptoService.getMasterKey();
if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
}
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount())
);
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
await super.saveAccountInformation(tokenResponse);
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);

View File

@@ -0,0 +1,41 @@
import { Utils } from "../../../platform/misc/utils";
// TODO: Tech Debt: potentially create a type Storage shape vs using a class here in the future
// type StorageShape {
// id: string;
// privateKey: string;
// }
// so we can get rid of the any type passed into fromJSON and coming out of ToJSON
export class AdminAuthRequestStorable {
id: string;
privateKey: Uint8Array;
constructor(init?: Partial<AdminAuthRequestStorable>) {
if (init) {
Object.assign(this, init);
}
}
toJSON() {
return {
id: this.id,
privateKey: Utils.fromBufferToByteString(this.privateKey),
};
}
static fromJSON(obj: any): AdminAuthRequestStorable {
if (obj == null) {
return null;
}
let privateKeyBuffer = null;
if (obj.privateKey) {
privateKeyBuffer = Utils.fromByteStringToArray(obj.privateKey);
}
return new AdminAuthRequestStorable({
id: obj.id,
privateKey: privateKeyBuffer,
});
}
}

View File

@@ -5,7 +5,14 @@ import { ForceResetPasswordReason } from "./force-reset-password-reason";
export class AuthResult {
captchaSiteKey = "";
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
/**
* @deprecated
* Replace with using AccountDecryptionOptions to determine if the user does
* not have a master password and is not using Key Connector.
* */
resetMasterPassword = false;
forcePasswordReset: ForceResetPasswordReason = ForceResetPasswordReason.None;
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
ssoEmail2FaSessionToken?: string;

View File

@@ -1,4 +1,4 @@
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { MasterKey, UserKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { AuthenticationType } from "../../enums/authentication-type";
import { TokenTwoFactorRequest } from "../request/identity-token/token-two-factor.request";
@@ -38,8 +38,9 @@ export class PasswordlessLogInCredentials {
public email: string,
public accessCode: string,
public authRequestId: string,
public decKey: SymmetricCryptoKey,
public localPasswordHash: string,
public decryptedUserKey: UserKey,
public decryptedMasterKey: MasterKey,
public decryptedMasterKeyHash: string,
public twoFactor?: TokenTwoFactorRequest
) {}
}

View File

@@ -0,0 +1,3 @@
export class KeyConnectorUserDecryptionOption {
constructor(public keyConnectorUrl: string) {}
}

View File

@@ -0,0 +1,7 @@
export class TrustedDeviceUserDecryptionOption {
constructor(
public hasAdminApproval: boolean,
public hasLoginApprovingDevice: boolean,
public hasManageResetPasswordPermission: boolean
) {}
}

View File

@@ -0,0 +1,15 @@
import { SecretVerificationRequest } from "./secret-verification.request";
export class UpdateDevicesTrustRequest extends SecretVerificationRequest {
currentDevice: DeviceKeysUpdateRequest;
otherDevices: OtherDeviceKeysUpdateRequest[];
}
export class DeviceKeysUpdateRequest {
encryptedPublicKey: string;
encryptedUserKey: string;
}
export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest {
id: string;
}

View File

@@ -2,6 +2,7 @@ import { KdfType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
import { UserDecryptionOptionsResponse } from "./user-decryption-options/user-decryption-options.response";
export class IdentityTokenResponse extends BaseResponse {
accessToken: string;
@@ -22,6 +23,8 @@ export class IdentityTokenResponse extends BaseResponse {
apiUseKeyConnector: boolean;
keyConnectorUrl: string;
userDecryptionOptions: UserDecryptionOptionsResponse;
constructor(response: any) {
super(response);
this.accessToken = response.access_token;
@@ -43,5 +46,11 @@ export class IdentityTokenResponse extends BaseResponse {
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy")
);
if (response.UserDecryptionOptions) {
this.userDecryptionOptions = new UserDecryptionOptionsResponse(
this.getResponseProperty("UserDecryptionOptions")
);
}
}
}

View File

@@ -0,0 +1,39 @@
import { Jsonify } from "type-fest";
import { DeviceType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { EncString } from "../../../platform/models/domain/enc-string";
export class ProtectedDeviceResponse extends BaseResponse {
constructor(response: Jsonify<ProtectedDeviceResponse>) {
super(response);
this.id = this.getResponseProperty("id");
this.name = this.getResponseProperty("name");
this.identifier = this.getResponseProperty("identifier");
this.type = this.getResponseProperty("type");
this.creationDate = new Date(this.getResponseProperty("creationDate"));
if (response.encryptedUserKey) {
this.encryptedUserKey = new EncString(this.getResponseProperty("encryptedUserKey"));
}
if (response.encryptedPublicKey) {
this.encryptedPublicKey = new EncString(this.getResponseProperty("encryptedPublicKey"));
}
}
id: string;
name: string;
type: DeviceType;
identifier: string;
creationDate: Date;
/**
* Intended to be the users symmetric key that is encrypted in some form, the current way to encrypt this is with
* the devices public key.
*/
encryptedUserKey: EncString;
/**
* Intended to be the public key that was generated for a device upon trust and encrypted. Currenly encrypted using
* a users symmetric key so that when trusted and unlocked a user can decrypt the public key for all their devices.
* This enabled a user to rotate the keys for all of their devices.
*/
encryptedPublicKey: EncString;
}

View File

@@ -0,0 +1,14 @@
import { BaseResponse } from "../../../../models/response/base.response";
export interface IKeyConnectorUserDecryptionOptionServerResponse {
KeyConnectorUrl: string;
}
export class KeyConnectorUserDecryptionOptionResponse extends BaseResponse {
keyConnectorUrl: string;
constructor(response: IKeyConnectorUserDecryptionOptionServerResponse) {
super(response);
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
}
}

View File

@@ -0,0 +1,35 @@
import { BaseResponse } from "../../../../models/response/base.response";
import { EncString } from "../../../../platform/models/domain/enc-string";
export interface ITrustedDeviceUserDecryptionOptionServerResponse {
HasAdminApproval: boolean;
HasLoginApprovingDevice: boolean;
HasManageResetPasswordPermission: boolean;
EncryptedPrivateKey?: string;
EncryptedUserKey?: string;
}
export class TrustedDeviceUserDecryptionOptionResponse extends BaseResponse {
hasAdminApproval: boolean;
hasLoginApprovingDevice: boolean;
hasManageResetPasswordPermission: boolean;
encryptedPrivateKey: EncString;
encryptedUserKey: EncString;
constructor(response: any) {
super(response);
this.hasAdminApproval = this.getResponseProperty("HasAdminApproval");
this.hasLoginApprovingDevice = this.getResponseProperty("HasLoginApprovingDevice");
this.hasManageResetPasswordPermission = this.getResponseProperty(
"HasManageResetPasswordPermission"
);
if (response.EncryptedPrivateKey) {
this.encryptedPrivateKey = new EncString(this.getResponseProperty("EncryptedPrivateKey"));
}
if (response.EncryptedUserKey) {
this.encryptedUserKey = new EncString(this.getResponseProperty("EncryptedUserKey"));
}
}
}

View File

@@ -0,0 +1,39 @@
import { BaseResponse } from "../../../../models/response/base.response";
import {
IKeyConnectorUserDecryptionOptionServerResponse,
KeyConnectorUserDecryptionOptionResponse,
} from "./key-connector-user-decryption-option.response";
import {
ITrustedDeviceUserDecryptionOptionServerResponse,
TrustedDeviceUserDecryptionOptionResponse,
} from "./trusted-device-user-decryption-option.response";
export interface IUserDecryptionOptionsServerResponse {
HasMasterPassword: boolean;
TrustedDeviceOption?: ITrustedDeviceUserDecryptionOptionServerResponse;
KeyConnectorOption?: IKeyConnectorUserDecryptionOptionServerResponse;
}
export class UserDecryptionOptionsResponse extends BaseResponse {
hasMasterPassword: boolean;
trustedDeviceOption?: TrustedDeviceUserDecryptionOptionResponse;
keyConnectorOption?: KeyConnectorUserDecryptionOptionResponse;
constructor(response: IUserDecryptionOptionsServerResponse) {
super(response);
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
if (response.TrustedDeviceOption) {
this.trustedDeviceOption = new TrustedDeviceUserDecryptionOptionResponse(
this.getResponseProperty("TrustedDeviceOption")
);
}
if (response.KeyConnectorOption) {
this.keyConnectorOption = new KeyConnectorUserDecryptionOptionResponse(
this.getResponseProperty("KeyConnectorOption")
);
}
}
}

View File

@@ -0,0 +1,81 @@
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { Utils } from "../../platform/misc/utils";
import {
UserKey,
SymmetricCryptoKey,
MasterKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthRequestResponse } from "../models/response/auth-request.response";
export class AuthRequestCryptoServiceImplementation implements AuthRequestCryptoServiceAbstraction {
constructor(private cryptoService: CryptoService) {}
async setUserKeyAfterDecryptingSharedUserKey(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: Uint8Array
) {
const userKey = await this.decryptPubKeyEncryptedUserKey(
authReqResponse.key,
authReqPrivateKey
);
await this.cryptoService.setUserKey(userKey);
}
async setKeysAfterDecryptingSharedMasterKeyAndHash(
authReqResponse: AuthRequestResponse,
authReqPrivateKey: Uint8Array
) {
const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash(
authReqResponse.key,
authReqResponse.masterPasswordHash,
authReqPrivateKey
);
// Decrypt and set user key in state
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
await this.cryptoService.setMasterKey(masterKey);
await this.cryptoService.setMasterKeyHash(masterKeyHash);
await this.cryptoService.setUserKey(userKey);
}
// Decryption helpers
async decryptPubKeyEncryptedUserKey(
pubKeyEncryptedUserKey: string,
privateKey: Uint8Array
): Promise<UserKey> {
const decryptedUserKeyBytes = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedUserKey,
privateKey
);
return new SymmetricCryptoKey(decryptedUserKeyBytes) as UserKey;
}
async decryptPubKeyEncryptedMasterKeyAndHash(
pubKeyEncryptedMasterKey: string,
pubKeyEncryptedMasterKeyHash: string,
privateKey: Uint8Array
): Promise<{ masterKey: MasterKey; masterKeyHash: string }> {
const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKey,
privateKey
);
const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt(
pubKeyEncryptedMasterKeyHash,
privateKey
);
const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey;
const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer);
return {
masterKey,
masterKeyHash,
};
}
}

View File

@@ -0,0 +1,165 @@
import { mock } from "jest-mock-extended";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { Utils } from "../../platform/misc/utils";
import {
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthRequestResponse } from "../models/response/auth-request.response";
import { AuthRequestCryptoServiceImplementation } from "./auth-request-crypto.service.implementation";
describe("AuthRequestCryptoService", () => {
let authReqCryptoService: AuthRequestCryptoServiceAbstraction;
const cryptoService = mock<CryptoService>();
let mockPrivateKey: Uint8Array;
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
authReqCryptoService = new AuthRequestCryptoServiceImplementation(cryptoService);
mockPrivateKey = new Uint8Array(64);
});
it("instantiates", () => {
expect(authReqCryptoService).not.toBeFalsy();
});
describe("setUserKeyAfterDecryptingSharedUserKey", () => {
it("decrypts and sets user key when given valid auth request response and private key", async () => {
// Arrange
const mockAuthReqResponse = {
key: "authReqPublicKeyEncryptedUserKey",
} as AuthRequestResponse;
const mockDecryptedUserKey = {} as UserKey;
jest
.spyOn(authReqCryptoService, "decryptPubKeyEncryptedUserKey")
.mockResolvedValueOnce(mockDecryptedUserKey);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
await authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
mockAuthReqResponse,
mockPrivateKey
);
// Assert
expect(authReqCryptoService.decryptPubKeyEncryptedUserKey).toBeCalledWith(
mockAuthReqResponse.key,
mockPrivateKey
);
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
});
});
describe("setKeysAfterDecryptingSharedMasterKeyAndHash", () => {
it("decrypts and sets master key and hash and user key when given valid auth request response and private key", async () => {
// Arrange
const mockAuthReqResponse = {
key: "authReqPublicKeyEncryptedMasterKey",
masterPasswordHash: "authReqPublicKeyEncryptedMasterKeyHash",
} as AuthRequestResponse;
const mockDecryptedMasterKey = {} as MasterKey;
const mockDecryptedMasterKeyHash = "mockDecryptedMasterKeyHash";
const mockDecryptedUserKey = {} as UserKey;
jest
.spyOn(authReqCryptoService, "decryptPubKeyEncryptedMasterKeyAndHash")
.mockResolvedValueOnce({
masterKey: mockDecryptedMasterKey,
masterKeyHash: mockDecryptedMasterKeyHash,
});
cryptoService.setMasterKey.mockResolvedValueOnce(undefined);
cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey);
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act
await authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
mockAuthReqResponse,
mockPrivateKey
);
// Assert
expect(authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith(
mockAuthReqResponse.key,
mockAuthReqResponse.masterPasswordHash,
mockPrivateKey
);
expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey);
expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash);
expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey);
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
});
});
describe("decryptAuthReqPubKeyEncryptedUserKey", () => {
it("returns a decrypted user key when given valid public key encrypted user key and an auth req private key", async () => {
// Arrange
const mockPubKeyEncryptedUserKey = "pubKeyEncryptedUserKey";
const mockDecryptedUserKeyBytes = new Uint8Array(64);
const mockDecryptedUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes) as UserKey;
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes);
// Act
const result = await authReqCryptoService.decryptPubKeyEncryptedUserKey(
mockPubKeyEncryptedUserKey,
mockPrivateKey
);
// Assert
expect(cryptoService.rsaDecrypt).toBeCalledWith(mockPubKeyEncryptedUserKey, mockPrivateKey);
expect(result).toEqual(mockDecryptedUserKey);
});
});
describe("decryptAuthReqPubKeyEncryptedMasterKeyAndHash", () => {
it("returns a decrypted master key and hash when given a valid public key encrypted master key, public key encrypted master key hash, and an auth req private key", async () => {
// Arrange
const mockPubKeyEncryptedMasterKey = "pubKeyEncryptedMasterKey";
const mockPubKeyEncryptedMasterKeyHash = "pubKeyEncryptedMasterKeyHash";
const mockDecryptedMasterKeyBytes = new Uint8Array(64);
const mockDecryptedMasterKey = new SymmetricCryptoKey(
mockDecryptedMasterKeyBytes
) as MasterKey;
const mockDecryptedMasterKeyHashBytes = new Uint8Array(64);
const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(mockDecryptedMasterKeyHashBytes);
cryptoService.rsaDecrypt
.mockResolvedValueOnce(mockDecryptedMasterKeyBytes)
.mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes);
// Act
const result = await authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash(
mockPubKeyEncryptedMasterKey,
mockPubKeyEncryptedMasterKeyHash,
mockPrivateKey
);
// Assert
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
1,
mockPubKeyEncryptedMasterKey,
mockPrivateKey
);
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
2,
mockPubKeyEncryptedMasterKeyHash,
mockPrivateKey
);
expect(result.masterKey).toEqual(mockDecryptedMasterKey);
expect(result.masterKeyHash).toEqual(mockDecryptedMasterKeyHash);
});
});
});

View File

@@ -16,9 +16,11 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
@@ -103,7 +105,9 @@ export class AuthService implements AuthServiceAbstraction {
protected i18nService: I18nService,
protected encryptService: EncryptService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected policyService: PolicyService
protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authReqCryptoService: AuthRequestCryptoServiceAbstraction
) {}
async logIn(
@@ -149,7 +153,10 @@ export class AuthService implements AuthServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService
this.keyConnectorService,
this.deviceTrustCryptoService,
this.authReqCryptoService,
this.i18nService
);
break;
case AuthenticationType.UserApi:
@@ -178,7 +185,7 @@ export class AuthService implements AuthServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this
this.deviceTrustCryptoService
);
break;
}
@@ -238,23 +245,32 @@ export class AuthService implements AuthServiceAbstraction {
}
async getAuthStatus(userId?: string): Promise<AuthenticationStatus> {
// If we don't have an access token or userId, we're logged out
const isAuthenticated = await this.stateService.getIsAuthenticated({ userId: userId });
if (!isAuthenticated) {
return AuthenticationStatus.LoggedOut;
}
// Keys aren't stored for a device that is locked or logged out
// Make sure we're logged in before checking this, otherwise we could mix up those states
const neverLock =
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
if (neverLock) {
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
// We should refactor here.
await this.cryptoService.getKey(KeySuffixOptions.Auto, userId);
// If we don't have a user key in memory, we're locked
if (!(await this.cryptoService.hasUserKeyInMemory(userId))) {
// Check if the user has vault timeout set to never and verify that
// they've never unlocked their vault
const neverLock =
(await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto, userId)) &&
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
if (neverLock) {
// Attempt to get the key from storage and set it in memory
const userKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Auto,
userId
);
await this.cryptoService.setUserKey(userKey, userId);
}
}
const hasKeyInMemory = await this.cryptoService.hasKeyInMemory(userId);
// We do another check here in case setting the auto key failed
const hasKeyInMemory = await this.cryptoService.hasUserKeyInMemory(userId);
if (!hasKeyInMemory) {
return AuthenticationStatus.Locked;
}
@@ -262,7 +278,7 @@ export class AuthService implements AuthServiceAbstraction {
return AuthenticationStatus.Unlocked;
}
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
email = email.trim().toLowerCase();
let kdf: KdfType = null;
let kdfConfig: KdfConfig = null;
@@ -281,7 +297,7 @@ export class AuthService implements AuthServiceAbstraction {
throw e;
}
}
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfConfig);
return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
}
async authResponsePushNotification(notification: AuthRequestPushNotification): Promise<any> {
@@ -298,22 +314,33 @@ export class AuthService implements AuthServiceAbstraction {
requestApproved: boolean
): Promise<AuthRequestResponse> {
const pubKey = Utils.fromB64ToArray(key);
const encryptedKey = await this.cryptoService.rsaEncrypt(
(
await this.cryptoService.getKey()
).encKey,
pubKey
);
let encryptedMasterPassword = null;
if ((await this.stateService.getKeyHash()) != null) {
encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
pubKey
);
const masterKey = await this.cryptoService.getMasterKey();
let keyToEncrypt;
let encryptedMasterKeyHash = null;
if (masterKey) {
keyToEncrypt = masterKey.encKey;
// Only encrypt the master password hash if masterKey exists as
// we won't have a masterKeyHash without a masterKey
const masterKeyHash = await this.stateService.getKeyHash();
if (masterKeyHash != null) {
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(masterKeyHash),
pubKey
);
}
} else {
const userKey = await this.cryptoService.getUserKey();
keyToEncrypt = userKey.key;
}
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
const request = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterPassword?.encryptedString,
encryptedMasterKeyHash?.encryptedString,
await this.appIdService.getAppId(),
requestApproved
);

View File

@@ -0,0 +1,215 @@
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import {
SymmetricCryptoKey,
DeviceKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
import {
DeviceKeysUpdateRequest,
UpdateDevicesTrustRequest,
} from "../models/request/update-devices-trust.request";
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
constructor(
private cryptoFunctionService: CryptoFunctionService,
private cryptoService: CryptoService,
private encryptService: EncryptService,
private stateService: StateService,
private appIdService: AppIdService,
private devicesApiService: DevicesApiServiceAbstraction,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService
) {}
/**
* @description Retrieves the users choice to trust the device which can only happen after decryption
* Note: this value should only be used once and then reset
*/
async getShouldTrustDevice(): Promise<boolean> {
return await this.stateService.getShouldTrustDevice();
}
async setShouldTrustDevice(value: boolean): Promise<void> {
await this.stateService.setShouldTrustDevice(value);
}
async trustDeviceIfRequired(): Promise<void> {
const shouldTrustDevice = await this.getShouldTrustDevice();
if (shouldTrustDevice) {
await this.trustDevice();
// reset the trust choice
await this.setShouldTrustDevice(false);
}
}
async trustDevice(): Promise<DeviceResponse> {
// Attempt to get user key
const userKey: UserKey = await this.cryptoService.getUserKey();
// If user key is not found, throw error
if (!userKey) {
throw new Error("User symmetric key not found");
}
// Generate deviceKey
const deviceKey = await this.makeDeviceKey();
// Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey
const [devicePublicKey, devicePrivateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(
2048
);
const [
devicePublicKeyEncryptedUserKey,
userKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey,
] = await Promise.all([
// Encrypt user key with the DevicePublicKey
this.cryptoService.rsaEncrypt(userKey.key, devicePublicKey),
// Encrypt devicePublicKey with user key
this.encryptService.encrypt(devicePublicKey, userKey),
// Encrypt devicePrivateKey with deviceKey
this.encryptService.encrypt(devicePrivateKey, deviceKey),
]);
// Send encrypted keys to server
const deviceIdentifier = await this.appIdService.getAppId();
const deviceResponse = await this.devicesApiService.updateTrustedDeviceKeys(
deviceIdentifier,
devicePublicKeyEncryptedUserKey.encryptedString,
userKeyEncryptedDevicePublicKey.encryptedString,
deviceKeyEncryptedDevicePrivateKey.encryptedString
);
// store device key in local/secure storage if enc keys posted to server successfully
await this.setDeviceKey(deviceKey);
this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted"));
return deviceResponse;
}
async rotateDevicesTrust(newUserKey: UserKey, masterPasswordHash: string): Promise<void> {
const currentDeviceKey = await this.getDeviceKey();
if (currentDeviceKey == null) {
// If the current device doesn't have a device key available to it, then we can't
// rotate any trust at all, so early return.
return;
}
// At this point of rotating their keys, they should still have their old user key in state
const oldUserKey = await this.stateService.getUserKey();
const deviceIdentifier = await this.appIdService.getAppId();
const secretVerificationRequest = new SecretVerificationRequest();
secretVerificationRequest.masterPasswordHash = masterPasswordHash;
// Get the keys that are used in rotating a devices keys from the server
const currentDeviceKeys = await this.devicesApiService.getDeviceKeys(
deviceIdentifier,
secretVerificationRequest
);
// Decrypt the existing device public key with the old user key
const decryptedDevicePublicKey = await this.encryptService.decryptToBytes(
currentDeviceKeys.encryptedPublicKey,
oldUserKey
);
// Encrypt the brand new user key with the now-decrypted public key for the device
const encryptedNewUserKey = await this.cryptoService.rsaEncrypt(
newUserKey.key,
decryptedDevicePublicKey
);
// Re-encrypt the device public key with the new user key
const encryptedDevicePublicKey = await this.encryptService.encrypt(
decryptedDevicePublicKey,
newUserKey
);
const currentDeviceUpdateRequest = new DeviceKeysUpdateRequest();
currentDeviceUpdateRequest.encryptedUserKey = encryptedNewUserKey.encryptedString;
currentDeviceUpdateRequest.encryptedPublicKey = encryptedDevicePublicKey.encryptedString;
// TODO: For device management, allow this method to take an array of device ids that can be looped over and individually rotated
// then it can be added to trustRequest.otherDevices.
const trustRequest = new UpdateDevicesTrustRequest();
trustRequest.masterPasswordHash = masterPasswordHash;
trustRequest.currentDevice = currentDeviceUpdateRequest;
trustRequest.otherDevices = [];
await this.devicesApiService.updateTrust(trustRequest);
}
async getDeviceKey(): Promise<DeviceKey> {
return await this.stateService.getDeviceKey();
}
private async setDeviceKey(deviceKey: DeviceKey | null): Promise<void> {
await this.stateService.setDeviceKey(deviceKey);
}
private async makeDeviceKey(): Promise<DeviceKey> {
// Create 512-bit device key
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
return deviceKey;
}
async decryptUserKeyWithDeviceKey(
encryptedDevicePrivateKey: EncString,
encryptedUserKey: EncString,
deviceKey?: DeviceKey
): Promise<UserKey | null> {
// If device key provided use it, otherwise try to retrieve from storage
deviceKey ||= await this.getDeviceKey();
if (!deviceKey) {
// User doesn't have a device key anymore so device is untrusted
return null;
}
try {
// attempt to decrypt encryptedDevicePrivateKey with device key
const devicePrivateKey = await this.encryptService.decryptToBytes(
encryptedDevicePrivateKey,
deviceKey
);
// Attempt to decrypt encryptedUserDataKey with devicePrivateKey
const userKey = await this.cryptoService.rsaDecrypt(
encryptedUserKey.encryptedString,
devicePrivateKey
);
return new SymmetricCryptoKey(userKey) as UserKey;
} catch (e) {
// If either decryption effort fails, we want to remove the device key
await this.setDeviceKey(null);
return null;
}
}
async supportsDeviceTrust(): Promise<boolean> {
const decryptionOptions = await this.stateService.getAccountDecryptionOptions();
return decryptionOptions?.trustedDeviceOption != null;
}
}

View File

@@ -0,0 +1,598 @@
import { matches, mock } from "jest-mock-extended";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { DeviceType } from "../../enums";
import { EncryptionType } from "../../enums/encryption-type.enum";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import {
SymmetricCryptoKey,
DeviceKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../../types/csprng";
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation";
describe("deviceTrustCryptoService", () => {
let deviceTrustCryptoService: DeviceTrustCryptoService;
const cryptoFunctionService = mock<CryptoFunctionService>();
const cryptoService = mock<CryptoService>();
const encryptService = mock<EncryptService>();
const stateService = mock<StateService>();
const appIdService = mock<AppIdService>();
const devicesApiService = mock<DevicesApiServiceAbstraction>();
const i18nService = mock<I18nService>();
const platformUtilsService = mock<PlatformUtilsService>();
beforeEach(() => {
jest.clearAllMocks();
deviceTrustCryptoService = new DeviceTrustCryptoService(
cryptoFunctionService,
cryptoService,
encryptService,
stateService,
appIdService,
devicesApiService,
i18nService,
platformUtilsService
);
});
it("instantiates", () => {
expect(deviceTrustCryptoService).not.toBeFalsy();
});
describe("User Trust Device Choice For Decryption", () => {
describe("getShouldTrustDevice", () => {
it("gets the user trust device choice for decryption from the state service", async () => {
const stateSvcGetShouldTrustDeviceSpy = jest.spyOn(stateService, "getShouldTrustDevice");
const expectedValue = true;
stateSvcGetShouldTrustDeviceSpy.mockResolvedValue(expectedValue);
const result = await deviceTrustCryptoService.getShouldTrustDevice();
expect(stateSvcGetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
expect(result).toEqual(expectedValue);
});
});
describe("setShouldTrustDevice", () => {
it("sets the user trust device choice for decryption in the state service", async () => {
const stateSvcSetShouldTrustDeviceSpy = jest.spyOn(stateService, "setShouldTrustDevice");
const newValue = true;
await deviceTrustCryptoService.setShouldTrustDevice(newValue);
expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledWith(newValue);
});
});
});
describe("trustDeviceIfRequired", () => {
it("should trust device and reset when getShouldTrustDevice returns true", async () => {
jest.spyOn(deviceTrustCryptoService, "getShouldTrustDevice").mockResolvedValue(true);
jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse);
jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue();
await deviceTrustCryptoService.trustDeviceIfRequired();
expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1);
expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1);
expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(false);
});
it("should not trust device nor reset when getShouldTrustDevice returns false", async () => {
const getShouldTrustDeviceSpy = jest
.spyOn(deviceTrustCryptoService, "getShouldTrustDevice")
.mockResolvedValue(false);
const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice");
const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice");
await deviceTrustCryptoService.trustDeviceIfRequired();
expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
expect(trustDeviceSpy).not.toHaveBeenCalled();
expect(setShouldTrustDeviceSpy).not.toHaveBeenCalled();
});
});
describe("Trusted Device Encryption core logic tests", () => {
const deviceKeyBytesLength = 64;
const userKeyBytesLength = 64;
describe("getDeviceKey", () => {
let existingDeviceKey: DeviceKey;
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
beforeEach(() => {
existingDeviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength) as CsprngArray
) as DeviceKey;
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
});
it("returns null when there is not an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
const deviceKey = await deviceTrustCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(deviceKey).toBeNull();
});
it("returns the device key when there is an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
const deviceKey = await deviceTrustCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(deviceKey).toEqual(existingDeviceKey);
});
});
describe("setDeviceKey", () => {
it("sets the device key in the state service", async () => {
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
const deviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength) as CsprngArray
) as DeviceKey;
// TypeScript will allow calling private methods if the object is of type 'any'
// This is a hacky workaround, but it allows for cleaner tests
await (deviceTrustCryptoService as any).setDeviceKey(deviceKey);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
});
});
describe("makeDeviceKey", () => {
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
const cryptoFuncSvcRandomBytesSpy = jest
.spyOn(cryptoFunctionService, "randomBytes")
.mockResolvedValue(mockRandomBytes);
// TypeScript will allow calling private methods if the object is of type 'any'
// This is a hacky workaround, but it allows for cleaner tests
const deviceKey = await (deviceTrustCryptoService as any).makeDeviceKey();
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
});
});
describe("trustDevice", () => {
let mockDeviceKeyRandomBytes: CsprngArray;
let mockDeviceKey: DeviceKey;
let mockUserKeyRandomBytes: CsprngArray;
let mockUserKey: UserKey;
const deviceRsaKeyLength = 2048;
let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array];
let mockDevicePrivateKey: Uint8Array;
let mockDevicePublicKey: Uint8Array;
let mockDevicePublicKeyEncryptedUserKey: EncString;
let mockUserKeyEncryptedDevicePublicKey: EncString;
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
Id: "mockId",
Name: "mockName",
Identifier: "mockIdentifier",
Type: "mockType",
CreationDate: "mockCreationDate",
});
const mockDeviceId = "mockDeviceId";
let makeDeviceKeySpy: jest.SpyInstance;
let rsaGenerateKeyPairSpy: jest.SpyInstance;
let cryptoSvcGetUserKeySpy: jest.SpyInstance;
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
let encryptServiceEncryptSpy: jest.SpyInstance;
let appIdServiceGetAppIdSpy: jest.SpyInstance;
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
beforeEach(() => {
// Setup all spies and default return values for the happy path
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
mockDeviceRsaKeyPair = [
new Uint8Array(deviceRsaKeyLength),
new Uint8Array(deviceRsaKeyLength),
];
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
mockDevicePublicKeyEncryptedUserKey = new EncString(
EncryptionType.Rsa2048_OaepSha1_B64,
"mockDevicePublicKeyEncryptedUserKey"
);
mockUserKeyEncryptedDevicePublicKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockUserKeyEncryptedDevicePublicKey"
);
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockDeviceKeyEncryptedDevicePrivateKey"
);
// TypeScript will allow calling private methods if the object is of type 'any'
makeDeviceKeySpy = jest
.spyOn(deviceTrustCryptoService as any, "makeDeviceKey")
.mockResolvedValue(mockDeviceKey);
rsaGenerateKeyPairSpy = jest
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
.mockResolvedValue(mockDeviceRsaKeyPair);
cryptoSvcGetUserKeySpy = jest
.spyOn(cryptoService, "getUserKey")
.mockResolvedValue(mockUserKey);
cryptoSvcRsaEncryptSpy = jest
.spyOn(cryptoService, "rsaEncrypt")
.mockResolvedValue(mockDevicePublicKeyEncryptedUserKey);
encryptServiceEncryptSpy = jest
.spyOn(encryptService, "encrypt")
.mockImplementation((plainValue, key) => {
if (plainValue === mockDevicePublicKey && key === mockUserKey) {
return Promise.resolve(mockUserKeyEncryptedDevicePublicKey);
}
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
}
});
appIdServiceGetAppIdSpy = jest
.spyOn(appIdService, "getAppId")
.mockResolvedValue(mockDeviceId);
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
.mockResolvedValue(mockDeviceResponse);
});
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
const response = await deviceTrustCryptoService.trustDevice();
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcGetUserKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
// RsaEncrypt must be called w/ a user key array buffer of 64 bytes
const userKeyKey: Uint8Array = cryptoSvcRsaEncryptSpy.mock.calls[0][0];
expect(userKeyKey.byteLength).toBe(64);
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
mockDeviceId,
mockDevicePublicKeyEncryptedUserKey.encryptedString,
mockUserKeyEncryptedDevicePublicKey.encryptedString,
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
);
expect(response).toBeInstanceOf(DeviceResponse);
expect(response).toEqual(mockDeviceResponse);
});
it("throws specific error if user key is not found", async () => {
// setup the spy to return null
cryptoSvcGetUserKeySpy.mockResolvedValue(null);
// check if the expected error is thrown
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
// reset the spy
cryptoSvcGetUserKeySpy.mockReset();
// setup the spy to return undefined
cryptoSvcGetUserKeySpy.mockResolvedValue(undefined);
// check if the expected error is thrown
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
});
const methodsToTestForErrorsOrInvalidReturns: any = [
{
method: "makeDeviceKey",
spy: () => makeDeviceKeySpy,
errorText: "makeDeviceKey error",
},
{
method: "rsaGenerateKeyPair",
spy: () => rsaGenerateKeyPairSpy,
errorText: "rsaGenerateKeyPair error",
},
{
method: "getUserKey",
spy: () => cryptoSvcGetUserKeySpy,
errorText: "getUserKey error",
},
{
method: "rsaEncrypt",
spy: () => cryptoSvcRsaEncryptSpy,
errorText: "rsaEncrypt error",
},
{
method: "encryptService.encrypt",
spy: () => encryptServiceEncryptSpy,
errorText: "encryptService.encrypt error",
},
];
describe.each(methodsToTestForErrorsOrInvalidReturns)(
"trustDevice error handling and invalid return testing",
({ method, spy, errorText }) => {
// ensures that error propagation works correctly
it(`throws an error if ${method} fails`, async () => {
const methodSpy = spy();
methodSpy.mockRejectedValue(new Error(errorText));
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(errorText);
});
test.each([null, undefined])(
`throws an error if ${method} returns %s`,
async (invalidValue) => {
const methodSpy = spy();
methodSpy.mockResolvedValue(invalidValue);
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow();
}
);
}
);
});
describe("decryptUserKeyWithDeviceKey", () => {
let mockDeviceKey: DeviceKey;
let mockEncryptedDevicePrivateKey: EncString;
let mockEncryptedUserKey: EncString;
let mockUserKey: UserKey;
beforeEach(() => {
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
const mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
mockEncryptedDevicePrivateKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockEncryptedDevicePrivateKey"
);
mockEncryptedUserKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockEncryptedUserKey"
);
jest.clearAllMocks();
});
it("returns null when device key isn't provided and isn't in state", async () => {
const getDeviceKeySpy = jest
.spyOn(deviceTrustCryptoService, "getDeviceKey")
.mockResolvedValue(null);
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
mockEncryptedDevicePrivateKey,
mockEncryptedUserKey
);
expect(result).toBeNull();
expect(getDeviceKeySpy).toHaveBeenCalledTimes(1);
});
it("successfully returns the user key when provided keys (including device key) can decrypt it", async () => {
const decryptToBytesSpy = jest
.spyOn(encryptService, "decryptToBytes")
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
const rsaDecryptSpy = jest
.spyOn(cryptoService, "rsaDecrypt")
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
mockEncryptedDevicePrivateKey,
mockEncryptedUserKey,
mockDeviceKey
);
expect(result).toEqual(mockUserKey);
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
});
it("successfully returns the user key when a device key is not provided (retrieves device key from state)", async () => {
const getDeviceKeySpy = jest
.spyOn(deviceTrustCryptoService, "getDeviceKey")
.mockResolvedValue(mockDeviceKey);
const decryptToBytesSpy = jest
.spyOn(encryptService, "decryptToBytes")
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
const rsaDecryptSpy = jest
.spyOn(cryptoService, "rsaDecrypt")
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
// Call without providing a device key
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
mockEncryptedDevicePrivateKey,
mockEncryptedUserKey
);
expect(getDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockUserKey);
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
});
it("returns null and removes device key when the decryption fails", async () => {
const decryptToBytesSpy = jest
.spyOn(encryptService, "decryptToBytes")
.mockRejectedValue(new Error("Decryption error"));
const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey");
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
mockEncryptedDevicePrivateKey,
mockEncryptedUserKey,
mockDeviceKey
);
expect(result).toBeNull();
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
expect(setDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(setDeviceKeySpy).toHaveBeenCalledWith(null);
});
});
describe("rotateDevicesTrust", () => {
let fakeNewUserKey: UserKey = null;
const FakeNewUserKeyMarker = 1;
const FakeOldUserKeyMarker = 5;
const FakeDecryptedPublicKeyMarker = 17;
beforeEach(() => {
const fakeNewUserKeyData = new Uint8Array(64);
fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1);
fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey;
});
it("does an early exit when the current device is not a trusted device", async () => {
stateService.getDeviceKey.mockResolvedValue(null);
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "");
expect(devicesApiService.updateTrust).not.toBeCalled();
});
describe("is on a trusted device", () => {
beforeEach(() => {
stateService.getDeviceKey.mockResolvedValue(
new SymmetricCryptoKey(new Uint8Array(deviceKeyBytesLength)) as DeviceKey
);
});
it("rotates current device keys and calls api service when the current device is trusted", async () => {
const currentEncryptedPublicKey = new EncString("2.cHVibGlj|cHVibGlj|cHVibGlj");
const currentEncryptedUserKey = new EncString("4.dXNlcg==");
const fakeOldUserKeyData = new Uint8Array(new Uint8Array(64));
// Fill the first byte with something identifiable
fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1);
// Mock the retrieval of a user key that differs from the new one passed into the method
stateService.getUserKey.mockResolvedValue(
new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey
);
appIdService.getAppId.mockResolvedValue("test_device_identifier");
devicesApiService.getDeviceKeys.mockImplementation((deviceIdentifier, secretRequest) => {
if (
deviceIdentifier !== "test_device_identifier" ||
secretRequest.masterPasswordHash !== "my_password_hash"
) {
return Promise.resolve(null);
}
return Promise.resolve(
new ProtectedDeviceResponse({
id: "",
creationDate: "",
identifier: "test_device_identifier",
name: "Firefox",
type: DeviceType.FirefoxBrowser,
encryptedPublicKey: currentEncryptedPublicKey.encryptedString,
encryptedUserKey: currentEncryptedUserKey.encryptedString,
})
);
});
// Mock the decryption of the public key with the old user key
encryptService.decryptToBytes.mockImplementationOnce((_encValue, privateKeyValue) => {
expect(privateKeyValue.key.byteLength).toBe(64);
expect(new Uint8Array(privateKeyValue.key)[0]).toBe(FakeOldUserKeyMarker);
const data = new Uint8Array(250);
data.fill(FakeDecryptedPublicKeyMarker, 0, 1);
return Promise.resolve(data);
});
// Mock the encryption of the new user key with the decrypted public key
cryptoService.rsaEncrypt.mockImplementationOnce((data, publicKey) => {
expect(data.byteLength).toBe(64); // New key should also be 64 bytes
expect(new Uint8Array(data)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1';
expect(new Uint8Array(publicKey)[0]).toBe(FakeDecryptedPublicKeyMarker);
return Promise.resolve(new EncString("4.ZW5jcnlwdGVkdXNlcg=="));
});
// Mock the reencryption of the device public key with the new user key
encryptService.encrypt.mockImplementationOnce((plainValue, key) => {
expect(plainValue).toBeInstanceOf(Uint8Array);
expect(new Uint8Array(plainValue as Uint8Array)[0]).toBe(FakeDecryptedPublicKeyMarker);
expect(new Uint8Array(key.key)[0]).toBe(FakeNewUserKeyMarker);
return Promise.resolve(
new EncString("2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj")
);
});
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "my_password_hash");
expect(devicesApiService.updateTrust).toBeCalledWith(
matches((updateTrustModel: UpdateDevicesTrustRequest) => {
return (
updateTrustModel.currentDevice.encryptedPublicKey ===
"2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj" &&
updateTrustModel.currentDevice.encryptedUserKey === "4.ZW5jcnlwdGVkdXNlcg=="
);
})
);
});
});
});
});
});

View File

@@ -0,0 +1,96 @@
import { ApiService } from "../../abstractions/api.service";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { ListResponse } from "../../models/response/list.response";
import { Utils } from "../../platform/misc/utils";
import { TrustedDeviceKeysRequest } from "../../services/devices/requests/trusted-device-keys.request";
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
const r = await this.apiService.send(
"GET",
"/devices/knowndevice",
null,
false,
true,
null,
(headers) => {
headers.set("X-Device-Identifier", deviceIdentifier);
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
}
);
return r as boolean;
}
/**
* Get device by identifier
* @param deviceIdentifier - client generated id (not device id in DB)
*/
async getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse> {
const r = await this.apiService.send(
"GET",
`/devices/identifier/${deviceIdentifier}`,
null,
true,
true
);
return new DeviceResponse(r);
}
async getDevices(): Promise<ListResponse<DeviceResponse>> {
const r = await this.apiService.send("GET", "/devices", null, true, true, null);
return new ListResponse(r, DeviceResponse);
}
async updateTrustedDeviceKeys(
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
): Promise<DeviceResponse> {
const request = new TrustedDeviceKeysRequest(
devicePublicKeyEncryptedUserKey,
userKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey
);
const result = await this.apiService.send(
"PUT",
`/devices/${deviceIdentifier}/keys`,
request,
true,
true
);
return new DeviceResponse(result);
}
async updateTrust(updateDevicesTrustRequestModel: UpdateDevicesTrustRequest): Promise<void> {
await this.apiService.send(
"POST",
"/devices/update-trust",
updateDevicesTrustRequestModel,
true,
false
);
}
async getDeviceKeys(
deviceIdentifier: string,
secretVerificationRequest: SecretVerificationRequest
): Promise<ProtectedDeviceResponse> {
const result = await this.apiService.send(
"POST",
`/devices/${deviceIdentifier}/retrieve-keys`,
secretVerificationRequest,
true,
true
);
return new ProtectedDeviceResponse(result);
}
}

View File

@@ -7,7 +7,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { MasterKey, SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { KdfConfig } from "../models/domain/kdf-config";
@@ -45,8 +45,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
async migrateUser() {
const organization = await this.getManagingOrganization();
const key = await this.cryptoService.getKey();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
const masterKey = await this.cryptoService.getMasterKey();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
try {
await this.apiService.postUserKeyToKeyConnector(
@@ -60,12 +60,13 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.apiService.postConvertToKeyConnector();
}
async getAndSetKey(url: string) {
// TODO: UserKey should be renamed to MasterKey and typed accordingly
async setMasterKeyFromUrl(url: string) {
try {
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
const k = new SymmetricCryptoKey(keyArr);
await this.cryptoService.setKey(k);
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
await this.cryptoService.setMasterKey(masterKey);
} catch (e) {
this.handleKeyConnectorError(e);
}
@@ -87,17 +88,18 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
const password = await this.cryptoFunctionService.randomBytes(64);
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const k = await this.cryptoService.makeKey(
const masterKey = await this.cryptoService.makeMasterKey(
Utils.fromBufferToB64(password),
await this.tokenService.getEmail(),
kdf,
kdfConfig
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64);
await this.cryptoService.setKey(k);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
await this.cryptoService.setMasterKey(masterKey);
const encKey = await this.cryptoService.makeEncKey(k);
await this.cryptoService.setEncKey(encKey[1].encryptedString);
const userKey = await this.cryptoService.makeUserKey(masterKey);
await this.cryptoService.setUserKey(userKey[0]);
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString);
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
@@ -109,7 +111,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
const keys = new KeysRequest(pubKey, privKey.encryptedString);
const setPasswordRequest = new SetKeyConnectorKeyRequest(
encKey[1].encryptedString,
userKey[1].encryptedString,
kdf,
kdfConfig,
orgId,

View File

@@ -0,0 +1,123 @@
import { mock, MockProxy } from "jest-mock-extended";
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
describe("PasswordResetEnrollmentServiceImplementation", () => {
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let stateService: MockProxy<StateService>;
let cryptoService: MockProxy<CryptoService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let i18nService: MockProxy<I18nService>;
let service: PasswordResetEnrollmentServiceImplementation;
beforeEach(() => {
organizationApiService = mock<OrganizationApiServiceAbstraction>();
stateService = mock<StateService>();
cryptoService = mock<CryptoService>();
organizationUserService = mock<OrganizationUserService>();
i18nService = mock<I18nService>();
service = new PasswordResetEnrollmentServiceImplementation(
organizationApiService,
stateService,
cryptoService,
organizationUserService,
i18nService
);
});
describe("enrollIfRequired", () => {
it("should not enroll when user is already enrolled in password reset", async () => {
const mockResponse = new OrganizationAutoEnrollStatusResponse({
ResetPasswordEnabled: true,
Id: "orgId",
});
organizationApiService.getAutoEnrollStatus.mockResolvedValue(mockResponse);
const enrollSpy = jest.spyOn(service, "enroll");
enrollSpy.mockResolvedValue();
await service.enrollIfRequired("ssoId");
expect(service.enroll).not.toHaveBeenCalled();
});
it("should enroll when user is not enrolled in password reset", async () => {
const mockResponse = new OrganizationAutoEnrollStatusResponse({
ResetPasswordEnabled: false,
Id: "orgId",
});
organizationApiService.getAutoEnrollStatus.mockResolvedValue(mockResponse);
const enrollSpy = jest.spyOn(service, "enroll");
enrollSpy.mockResolvedValue();
await service.enrollIfRequired("ssoId");
expect(service.enroll).toHaveBeenCalled();
});
});
describe("enroll", () => {
it("should throw an error if the organization keys are not found", async () => {
organizationApiService.getKeys.mockResolvedValue(null);
i18nService.t.mockReturnValue("resetPasswordOrgKeysError");
const result = () => service.enroll("orgId");
await expect(result).rejects.toThrowError("resetPasswordOrgKeysError");
});
it("should enroll the user when no user id or key is provided", async () => {
const orgKeyResponse = {
publicKey: "publicKey",
privateKey: "privateKey",
};
const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
stateService.getUserId.mockResolvedValue("userId");
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
await service.enroll("orgId");
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).toHaveBeenCalledWith(
"orgId",
"userId",
expect.objectContaining({
resetPasswordKey: encryptedKey.encryptedString,
})
);
});
it("should enroll the user when a user id and key is provided", async () => {
const orgKeyResponse = {
publicKey: "publicKey",
privateKey: "privateKey",
};
const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
await service.enroll("orgId", "userId", { key: "key" } as any);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment
).toHaveBeenCalledWith(
"orgId",
"userId",
expect.objectContaining({
resetPasswordKey: encryptedKey.encryptedString,
})
);
});
});
});

View File

@@ -0,0 +1,56 @@
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../abstractions/organization-user/requests";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
export class PasswordResetEnrollmentServiceImplementation
implements PasswordResetEnrollmentServiceAbstraction
{
constructor(
protected organizationApiService: OrganizationApiServiceAbstraction,
protected stateService: StateService,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected i18nService: I18nService
) {}
async enrollIfRequired(organizationSsoIdentifier: string): Promise<void> {
const orgAutoEnrollStatusResponse = await this.organizationApiService.getAutoEnrollStatus(
organizationSsoIdentifier
);
if (!orgAutoEnrollStatusResponse.resetPasswordEnabled) {
await this.enroll(orgAutoEnrollStatusResponse.id, null, null);
}
}
async enroll(organizationId: string): Promise<void>;
async enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
async enroll(organizationId: string, userId?: string, userKey?: UserKey): Promise<void> {
const orgKeyResponse = await this.organizationApiService.getKeys(organizationId);
if (orgKeyResponse == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
userId = userId ?? (await this.stateService.getUserId());
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
// RSA Encrypt user's userKey.key with organization public key
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey);
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
organizationId,
userId,
resetRequest
);
}
}

View File

@@ -1,5 +1,6 @@
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { Verification } from "../../../types/verification";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
@@ -13,6 +14,7 @@ import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
*/
export class UserVerificationService implements UserVerificationServiceAbstraction {
constructor(
private stateService: StateService,
private cryptoService: CryptoService,
private i18nService: I18nService,
private userVerificationApiService: UserVerificationApiServiceAbstraction
@@ -37,9 +39,18 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
if (verification.type === VerificationType.OTP) {
request.otp = verification.secret;
} else {
let masterKey = await this.cryptoService.getMasterKey();
if (!masterKey && !alreadyHashed) {
masterKey = await this.cryptoService.makeMasterKey(
verification.secret,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig()
);
}
request.masterPasswordHash = alreadyHashed
? verification.secret
: await this.cryptoService.hashPassword(verification.secret, null);
: await this.cryptoService.hashMasterKey(verification.secret, masterKey);
}
return request;
@@ -61,13 +72,23 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
throw new Error(this.i18nService.t("invalidVerificationCode"));
}
} else {
let masterKey = await this.cryptoService.getMasterKey();
if (!masterKey) {
masterKey = await this.cryptoService.makeMasterKey(
verification.secret,
await this.stateService.getEmail(),
await this.stateService.getKdfType(),
await this.stateService.getKdfConfig()
);
}
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
verification.secret,
null
masterKey
);
if (!passwordValid) {
throw new Error(this.i18nService.t("invalidMasterPassword"));
}
this.cryptoService.setMasterKey(masterKey);
}
return true;
}
@@ -76,6 +97,30 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
await this.userVerificationApiService.postAccountRequestOTP();
}
/**
* Check if user has master password or can only use passwordless technologies to log in
* Note: This only checks the server, not the local state
* @param userId The user id to check. If not provided, the current user is used
* @returns True if the user has a master password
*/
async hasMasterPassword(userId?: string): Promise<boolean> {
const decryptionOptions = await this.stateService.getAccountDecryptionOptions({ userId });
if (decryptionOptions?.hasMasterPassword != undefined) {
return decryptionOptions.hasMasterPassword;
}
// TODO: PM-3518 - Left for backwards compatibility, remove after 2023.12.0
return !(await this.stateService.getUsesKeyConnector({ userId }));
}
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {
return (
(await this.hasMasterPassword(userId)) &&
(await this.cryptoService.getMasterKeyHash()) != null
);
}
private validateInput(verification: Verification) {
if (verification?.secret == null || verification.secret === "") {
if (verification.type === VerificationType.OTP) {

View File

@@ -23,3 +23,16 @@ export enum DeviceType {
SDK = 21,
Server = 22,
}
export const MobileDeviceTypes: Set<DeviceType> = new Set([
DeviceType.Android,
DeviceType.iOS,
DeviceType.AndroidAmazon,
]);
export const DesktopDeviceTypes: Set<DeviceType> = new Set([
DeviceType.WindowsDesktop,
DeviceType.MacOsDesktop,
DeviceType.LinuxDesktop,
DeviceType.UWP,
]);

View File

@@ -10,6 +10,7 @@ export enum EventType {
User_ClientExportedVault = 1007,
User_UpdatedTempPassword = 1008,
User_MigratedKeyToKeyConnector = 1009,
User_RequestedDeviceApproval = 1010,
Cipher_Created = 1100,
Cipher_Updated = 1101,
@@ -51,6 +52,8 @@ export enum EventType {
OrganizationUser_FirstSsoLogin = 1510,
OrganizationUser_Revoked = 1511,
OrganizationUser_Restored = 1512,
OrganizationUser_ApprovedAuthRequest = 1513,
OrganizationUser_RejectedAuthRequest = 1514,
Organization_Updated = 1600,
Organization_PurgedVault = 1601,

View File

@@ -1,4 +1,5 @@
export enum KeySuffixOptions {
Auto = "auto",
Biometric = "biometric",
Pin = "pin",
}

View File

@@ -5,82 +5,420 @@ import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { KeySuffixOptions, KdfType, HashPurpose } from "../../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import {
MasterKey,
OrgKey,
PinKey,
ProviderKey,
SymmetricCryptoKey,
UserKey,
} from "../models/domain/symmetric-crypto-key";
export abstract class CryptoService {
setKey: (key: SymmetricCryptoKey) => Promise<any>;
setKeyHash: (keyHash: string) => Promise<void>;
setEncKey: (encKey: string) => Promise<void>;
setEncPrivateKey: (encPrivateKey: string) => Promise<void>;
/**
* Sets the provided user key and stores
* any other necessary versions (such as auto, biometrics,
* or pin)
* @param key The user key to set
* @param userId The desired user
*/
setUserKey: (key: UserKey, userId?: string) => Promise<void>;
/**
* Gets the user key from memory and sets it again,
* kicking off a refresh of any additional keys
* (such as auto, biometrics, or pin)
*/
/**
* Check if the current sessions has ever had a user key, i.e. has ever been unlocked/decrypted.
* This is key for differentiating between TDE locked and standard locked states.
* @param userId The desired user
* @returns True if the current session has ever had a user key
*/
getEverHadUserKey: (userId?: string) => Promise<boolean>;
refreshAdditionalKeys: () => Promise<void>;
/**
* Retrieves the user key
* @param userId The desired user
* @returns The user key
*/
getUserKey: (userId?: string) => Promise<UserKey>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
* @param userId The desired user
*/
getUserKeyWithLegacySupport: (userId?: string) => Promise<UserKey>;
/**
* Retrieves the user key from storage
* @param keySuffix The desired version of the user's key to retrieve
* @param userId The desired user
* @returns The user key
*/
getUserKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<UserKey>;
/**
* @returns True if the user key is available
*/
hasUserKey: () => Promise<boolean>;
/**
* @param userId The desired user
* @returns True if the user key is set in memory
*/
hasUserKeyInMemory: (userId?: string) => Promise<boolean>;
/**
* @param keySuffix The desired version of the user's key to check
* @param userId The desired user
* @returns True if the provided version of the user key is stored
*/
hasUserKeyStored: (keySuffix: KeySuffixOptions, userId?: string) => Promise<boolean>;
/**
* Generates a new user key
* @param masterKey The user's master key
* @returns A new user key and the master key protected version of it
*/
makeUserKey: (key: MasterKey) => Promise<[UserKey, EncString]>;
/**
* Clears the user key
* @param clearStoredKeys Clears all stored versions of the user keys as well,
* such as the biometrics key
* @param userId The desired user
*/
clearUserKey: (clearSecretStorage?: boolean, userId?: string) => Promise<void>;
/**
* Clears the user's stored version of the user key
* @param keySuffix The desired version of the key to clear
* @param userId The desired user
*/
clearStoredUserKey: (keySuffix: KeySuffixOptions, userId?: string) => Promise<void>;
/**
* Stores the master key encrypted user key
* @param userKeyMasterKey The master key encrypted user key to set
* @param userId The desired user
*/
setMasterKeyEncryptedUserKey: (UserKeyMasterKey: string, userId?: string) => Promise<void>;
/**
* Sets the user's master key
* @param key The user's master key to set
* @param userId The desired user
*/
setMasterKey: (key: MasterKey, userId?: string) => Promise<void>;
/**
* @param userId The desired user
* @returns The user's master key
*/
getMasterKey: (userId?: string) => Promise<MasterKey>;
/**
* @param password The user's master password that will be used to derive a master key if one isn't found
* @param userId The desired user
*/
getOrDeriveMasterKey: (password: string, userId?: string) => Promise<MasterKey>;
/**
* Generates a master key from the provided password
* @param password The user's master password
* @param email The user's email
* @param kdf The user's selected key derivation function to use
* @param KdfConfig The user's key derivation function configuration
* @returns A master key derived from the provided password
*/
makeMasterKey: (
password: string,
email: string,
kdf: KdfType,
KdfConfig: KdfConfig
) => Promise<MasterKey>;
/**
* Clears the user's master key
* @param userId The desired user
*/
clearMasterKey: (userId?: string) => Promise<void>;
/**
* Encrypts the existing (or provided) user key with the
* provided master key
* @param masterKey The user's master key
* @param userKey The user key
* @returns The user key and the master key protected version of it
*/
encryptUserKeyWithMasterKey: (
masterKey: MasterKey,
userKey?: UserKey
) => Promise<[UserKey, EncString]>;
/**
* Decrypts the user key with the provided master key
* @param masterKey The user's master key
* @param userKey The user's encrypted symmetric key
* @param userId The desired user
* @returns The user key
*/
decryptUserKeyWithMasterKey: (
masterKey: MasterKey,
userKey?: EncString,
userId?: string
) => Promise<UserKey>;
/**
* Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending
* on the hashPurpose provided.
* @param password The user's master password
* @param key The user's master key
* @param hashPurpose The iterations to use for the hash
* @returns The user's master password hash
*/
hashMasterKey: (password: string, key: MasterKey, hashPurpose?: HashPurpose) => Promise<string>;
/**
* Sets the user's master password hash
* @param keyHash The user's master password hash to set
*/
setMasterKeyHash: (keyHash: string) => Promise<void>;
/**
* @returns The user's master password hash
*/
getMasterKeyHash: () => Promise<string>;
/**
* Clears the user's stored master password hash
* @param userId The desired user
*/
clearMasterKeyHash: (userId?: string) => Promise<void>;
/**
* Compares the provided master password to the stored password hash and server password hash.
* Updates the stored hash if outdated.
* @param masterPassword The user's master password
* @param key The user's master key
* @returns True if the provided master password matches either the stored
* key hash or the server key hash
*/
compareAndUpdateKeyHash: (masterPassword: string, masterKey: MasterKey) => Promise<boolean>;
/**
* Stores the encrypted organization keys and clears any decrypted
* organization keys currently in memory
* @param orgs The organizations to set keys for
* @param providerOrgs The provider organizations to set keys for
*/
setOrgKeys: (
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[]
) => Promise<void>;
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
getKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
getKeyHash: () => Promise<string>;
compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise<boolean>;
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
getPublicKey: () => Promise<Uint8Array>;
getPrivateKey: () => Promise<Uint8Array>;
getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise<string[]>;
/**
* Returns the organization's symmetric key
* @param orgId The desired organization
* @returns The organization's symmetric key
*/
getOrgKey: (orgId: string) => Promise<OrgKey>;
/**
* @returns A map of the organization Ids to their symmetric keys
*/
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
getKeyForUserEncryption: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
hasKey: () => Promise<boolean>;
hasKeyInMemory: (userId?: string) => Promise<boolean>;
hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<boolean>;
hasEncKey: () => Promise<boolean>;
clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise<any>;
clearKeyHash: () => Promise<any>;
clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearProviderKeys: (memoryOnly?: boolean) => Promise<any>;
clearPinProtectedKey: () => Promise<any>;
clearKeys: (userId?: string) => Promise<any>;
toggleKey: () => Promise<any>;
makeKey: (
password: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig
) => Promise<SymmetricCryptoKey>;
makeKeyFromPin: (
/**
* Uses the org key to derive a new symmetric key for encrypting data
* @param orgKey The organization's symmetric key
*/
makeDataEncKey: <T extends UserKey | OrgKey>(key: T) => Promise<[SymmetricCryptoKey, EncString]>;
/**
* Clears the user's stored organization keys
* @param memoryOnly Clear only the in-memory keys
* @param userId The desired user
*/
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<void>;
/**
* Stores the encrypted provider keys and clears any decrypted
* provider keys currently in memory
* @param providers The providers to set keys for
*/
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
/**
* @param providerId The desired provider
* @returns The provider's symmetric key
*/
getProviderKey: (providerId: string) => Promise<ProviderKey>;
/**
* @returns A map of the provider Ids to their symmetric keys
*/
getProviderKeys: () => Promise<Map<string, ProviderKey>>;
/**
* @param memoryOnly Clear only the in-memory keys
* @param userId The desired user
*/
clearProviderKeys: (memoryOnly?: boolean, userId?: string) => Promise<void>;
/**
* Returns the public key from memory. If not available, extracts it
* from the private key and stores it in memory
* @returns The user's public key
*/
getPublicKey: () => Promise<Uint8Array>;
/**
* Creates a new organization key and encrypts it with the user's public key.
* This method can also return Provider keys for creating new Provider users.
* @returns The new encrypted org key and the decrypted key itself
*/
makeOrgKey: <T extends OrgKey | ProviderKey>() => Promise<[EncString, T]>;
/**
* Sets the the user's encrypted private key in storage and
* clears the decrypted private key from memory
* Note: does not clear the private key if null is provided
* @param encPrivateKey An encrypted private key
*/
setPrivateKey: (encPrivateKey: string) => Promise<void>;
/**
* Returns the private key from memory. If not available, decrypts it
* from storage and stores it in memory
* @returns The user's private key
*/
getPrivateKey: () => Promise<Uint8Array>;
/**
* Generates a fingerprint phrase for the user based on their public key
* @param fingerprintMaterial Fingerprint material
* @param publicKey The user's public key
* @returns The user's fingerprint phrase
*/
getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise<string[]>;
/**
* Generates a new keypair
* @param key A key to encrypt the private key with. If not provided,
* defaults to the user key
* @returns A new keypair: [publicKey in Base64, encrypted privateKey]
*/
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
/**
* Clears the user's key pair
* @param memoryOnly Clear only the in-memory keys
* @param userId The desired user
*/
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<void[]>;
/**
* @param pin The user's pin
* @param salt The user's salt
* @param kdf The user's kdf
* @param kdfConfig The user's kdf config
* @returns A key derived from the user's pin
*/
makePinKey: (pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig) => Promise<PinKey>;
/**
* Clears the user's pin keys from storage
* Note: This will remove the stored pin and as a result,
* disable pin protection for the user
* @param userId The desired user
*/
clearPinKeys: (userId?: string) => Promise<void>;
/**
* Decrypts the user key with their pin
* @param pin The user's PIN
* @param salt The user's salt
* @param kdf The user's KDF
* @param kdfConfig The user's KDF config
* @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided
* it will be retrieved from storage
* @returns The decrypted user key
*/
decryptUserKeyWithPin: (
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig,
protectedKeyCs?: EncString
) => Promise<SymmetricCryptoKey>;
makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>;
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
makePinKey: (
) => Promise<UserKey>;
/**
* Creates a new Pin key that encrypts the user key instead of the
* master key. Clears the old Pin key from state.
* @param masterPasswordOnRestart True if Master Password on Restart is enabled
* @param pin User's PIN
* @param email User's email
* @param kdf User's KdfType
* @param kdfConfig User's KdfConfig
* @param oldPinKey The old Pin key from state (retrieved from different
* places depending on if Master Password on Restart was enabled)
* @returns The user key
*/
decryptAndMigrateOldPinKey: (
masterPasswordOnRestart: boolean,
pin: string,
email: string,
kdf: KdfType,
kdfConfig: KdfConfig,
oldPinKey: EncString
) => Promise<UserKey>;
/**
* Replaces old master auto keys with new user auto keys
*/
migrateAutoKeyIfNeeded: (userId?: string) => Promise<void>;
/**
* @param keyMaterial The key material to derive the send key from
* @returns A new send key
*/
makeSendKey: (keyMaterial: Uint8Array) => Promise<SymmetricCryptoKey>;
/**
* Clears all of the user's keys from storage
* @param userId The user's Id
*/
clearKeys: (userId?: string) => Promise<any>;
/**
* RSA encrypts a value.
* @param data The data to encrypt
* @param publicKey The public key to use for encryption, if not provided, the user's public key will be used
* @returns The encrypted data
*/
rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise<EncString>;
/**
* Decrypts a value using RSA.
* @param encValue The encrypted value to decrypt
* @param privateKeyValue The private key to use for decryption
* @returns The decrypted value
*/
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
randomNumber: (min: number, max: number) => Promise<number>;
/**
* Initialize all necessary crypto keys needed for a new account.
* Warning! This completely replaces any existing keys!
* @returns The user's newly created public key, private key, and encrypted private key
*/
initAccount: () => Promise<{
userKey: UserKey;
publicKey: string;
privateKey: EncString;
}>;
/**
* @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
*/
decryptMasterKeyWithPin: (
pin: string,
salt: string,
kdf: KdfType,
kdfConfig: KdfConfig
) => Promise<SymmetricCryptoKey>;
makeSendKey: (keyMaterial: Uint8Array) => Promise<SymmetricCryptoKey>;
hashPassword: (
password: string,
key: SymmetricCryptoKey,
hashPurpose?: HashPurpose
) => Promise<string>;
makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>;
remakeEncKey: (
key: SymmetricCryptoKey,
encKey?: SymmetricCryptoKey
) => Promise<[SymmetricCryptoKey, EncString]>;
kdfConfig: KdfConfig,
protectedKeyCs?: EncString
) => Promise<MasterKey>;
/**
* Previously, the master key was used for any additional key like the biometrics or pin key.
* We have switched to using the user key for these purposes. This method is for clearing the state
* of the older keys on logout or post migration.
* @param keySuffix The desired type of key to clear
* @param userId The desired user
*/
clearDeprecatedKeys: (keySuffix: KeySuffixOptions, userId?: string) => Promise<void>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.encrypt
*/
encrypt: (plainValue: string | Uint8Array, key?: SymmetricCryptoKey) => Promise<EncString>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.encryptToBytes
*/
encryptToBytes: (plainValue: Uint8Array, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise<EncString>;
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.decryptToBytes
*/
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<Uint8Array>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.decryptToUtf8
*/
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
/**
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.decryptToBytes
*/
decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise<Uint8Array>;
randomNumber: (min: number, max: number) => Promise<number>;
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
}

View File

@@ -5,6 +5,7 @@ import { OrganizationData } from "../../admin-console/models/data/organization.d
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
@@ -23,10 +24,19 @@ import { CipherView } from "../../vault/models/view/cipher.view";
import { CollectionView } from "../../vault/models/view/collection.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { ServerConfigData } from "../models/data/server-config.data";
import { Account, AccountSettingsSettings } from "../models/domain/account";
import {
Account,
AccountDecryptionOptions,
AccountSettingsSettings,
} from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options";
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import {
DeviceKey,
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../models/domain/symmetric-crypto-key";
export abstract class StateService<T extends Account = Account> {
accounts$: Observable<{ [userId: string]: T }>;
@@ -71,24 +81,106 @@ export abstract class StateService<T extends Account = Account> {
setCollapsedGroupings: (value: string[], options?: StorageOptions) => Promise<void>;
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
/**
* gets the user key
*/
getUserKey: (options?: StorageOptions) => Promise<UserKey>;
/**
* Sets the user key
*/
setUserKey: (value: UserKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's master key
*/
getMasterKey: (options?: StorageOptions) => Promise<MasterKey>;
/**
* Sets the user's master key
*/
setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user key encrypted by the master key
*/
getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user key encrypted by the master key
*/
setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's auto key
*/
getUserKeyAutoUnlock: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user's auto key
*/
setUserKeyAutoUnlock: (value: string, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's biometric key
*/
getUserKeyBiometric: (options?: StorageOptions) => Promise<string>;
/**
* Checks if the user has a biometric key available
*/
hasUserKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
/**
* Sets the user's biometric key
*/
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is disabled
*/
getPinKeyEncryptedUserKey: (options?: StorageOptions) => Promise<EncString>;
/**
* Sets the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is disabled
*/
setPinKeyEncryptedUserKey: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* Gets the ephemeral version of the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is enabled
*/
getPinKeyEncryptedUserKeyEphemeral: (options?: StorageOptions) => Promise<EncString>;
/**
* Sets the ephemeral version of the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is enabled
*/
setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For migration purposes only, use getUserKeyMasterKey instead
*/
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use setUserKeyMasterKey instead
*/
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For legacy purposes only, use getMasterKey instead
*/
getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For migration purposes only, use getUserKeyAuto instead
*/
getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use setUserKeyAuto instead
*/
setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise<void>;
getCryptoMasterKeyB64: (options?: StorageOptions) => Promise<string>;
setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated For migration purposes only, use getUserKeyBiometric instead
*/
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use hasUserKeyBiometric instead
*/
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
/**
* @deprecated For migration purposes only, use setUserKeyBiometric instead
*/
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
getDecryptedCollections: (options?: StorageOptions) => Promise<CollectionView[]>;
setDecryptedCollections: (value: CollectionView[], options?: StorageOptions) => Promise<void>;
getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
setDecryptedCryptoSymmetricKey: (
value: SymmetricCryptoKey,
options?: StorageOptions
) => Promise<void>;
getDecryptedOrganizationKeys: (
options?: StorageOptions
) => Promise<Map<string, SymmetricCryptoKey>>;
@@ -103,7 +195,13 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[],
options?: StorageOptions
) => Promise<void>;
/**
* @deprecated For migration purposes only, use getDecryptedUserKeyPin instead
*/
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
/**
* @deprecated For migration purposes only, use setDecryptedUserKeyPin instead
*/
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this, use PolicyService
@@ -164,7 +262,21 @@ export abstract class StateService<T extends Account = Account> {
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise<void>;
setDeviceKey: (value: DeviceKey | null, options?: StorageOptions) => Promise<void>;
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
setAdminAuthRequest: (
adminAuthRequest: AdminAuthRequestStorable,
options?: StorageOptions
) => Promise<void>;
getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>;
setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>;
getAccountDecryptionOptions: (
options?: StorageOptions
) => Promise<AccountDecryptionOptions | null>;
setAccountDecryptionOptions: (
value: AccountDecryptionOptions,
options?: StorageOptions
) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>;
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
@@ -205,8 +317,6 @@ export abstract class StateService<T extends Account = Account> {
value: { [id: string]: CollectionData },
options?: StorageOptions
) => Promise<void>;
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use FolderService
*/
@@ -232,7 +342,13 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[],
options?: StorageOptions
) => Promise<void>;
/**
* @deprecated For migration purposes only, use getEncryptedUserKeyPin instead
*/
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use setEncryptedUserKeyPin instead
*/
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use PolicyService
@@ -269,6 +385,8 @@ export abstract class StateService<T extends Account = Account> {
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
getEverHadUserKey: (options?: StorageOptions) => Promise<boolean>;
setEverHadUserKey: (value: boolean, options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
getForcePasswordResetReason: (options?: StorageOptions) => Promise<ForceResetPasswordReason>;
@@ -327,7 +445,13 @@ export abstract class StateService<T extends Account = Account> {
setUsernameGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
getGeneratorOptions: (options?: StorageOptions) => Promise<any>;
setGeneratorOptions: (value: any, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's Pin, encrypted by the user key
*/
getProtectedPin: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user's Pin, encrypted by the user key
*/
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
@@ -353,6 +477,11 @@ export abstract class StateService<T extends Account = Account> {
setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise<void>;
getSsoState: (options?: StorageOptions) => Promise<string>;
setSsoState: (value: string, options?: StorageOptions) => Promise<void>;
getUserSsoOrganizationIdentifier: (options?: StorageOptions) => Promise<string>;
setUserSsoOrganizationIdentifier: (
value: string | null,
options?: StorageOptions
) => Promise<void>;
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;

View File

@@ -1,8 +1,9 @@
import { makeStaticByteArray } from "../../../../spec";
import { CsprngArray } from "../../../types/csprng";
import { Utils } from "../../misc/utils";
import { AccountKeys, EncryptionPair } from "./account";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
describe("AccountKeys", () => {
describe("toJSON", () => {
@@ -22,6 +23,23 @@ describe("AccountKeys", () => {
const json = JSON.stringify(keys);
expect(json).toContain('"publicKey":"hello"');
});
// As the accountKeys.toJSON doesn't really serialize the device key
// this method just checks the persistence of the deviceKey
it("should persist deviceKey", () => {
// Arrange
const accountKeys = new AccountKeys();
const deviceKeyBytesLength = 64;
accountKeys.deviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
) as DeviceKey;
// Act
const serializedKeys = accountKeys.toJSON();
// Assert
expect(serializedKeys.deviceKey).toEqual(accountKeys.deviceKey);
});
});
describe("fromJSON", () => {
@@ -57,5 +75,24 @@ describe("AccountKeys", () => {
} as any);
expect(spy).toHaveBeenCalled();
});
it("should deserialize deviceKey", () => {
// Arrange
const expectedKeyB64 =
"ZJNnhx9BbJeb2EAq1hlMjqt6GFsg9G/GzoFf6SbPKsaiMhKGDcbHcwcyEg56Lh8lfilpZz4SRM6UA7oFCg+lSg==";
const symmetricCryptoKeyFromJsonSpy = jest.spyOn(SymmetricCryptoKey, "fromJSON");
// Act
const accountKeys = AccountKeys.fromJSON({
deviceKey: {
keyB64: expectedKeyB64,
},
} as any);
// Assert
expect(symmetricCryptoKeyFromJsonSpy).toHaveBeenCalled();
expect(accountKeys.deviceKey.keyB64).toEqual(expectedKeyB64);
});
});
});

View File

@@ -6,8 +6,12 @@ import { PolicyData } from "../../../admin-console/models/data/policy.data";
import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { Policy } from "../../../admin-console/models/domain/policy";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { UserDecryptionOptionsResponse } from "../../../auth/models/response/user-decryption-options/user-decryption-options.response";
import { KdfType, UriMatchType } from "../../../enums";
import { EventData } from "../../../models/data/event.data";
import { GeneratedPasswordHistory } from "../../../tools/generator/password";
@@ -22,8 +26,8 @@ import { CollectionView } from "../../../vault/models/view/collection.view";
import { Utils } from "../../misc/utils";
import { ServerConfigData } from "../../models/data/server-config.data";
import { EncString } from "./enc-string";
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
import { EncryptedString, EncString } from "./enc-string";
import { MasterKey, SymmetricCryptoKey, UserKey } from "./symmetric-crypto-key";
export class EncryptionPair<TEncrypted, TDecrypted> {
encrypted?: TEncrypted;
@@ -99,15 +103,10 @@ export class AccountData {
}
export class AccountKeys {
cryptoMasterKey?: SymmetricCryptoKey;
cryptoMasterKeyAuto?: string;
cryptoMasterKeyB64?: string;
cryptoMasterKeyBiometric?: string;
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
string,
SymmetricCryptoKey
>();
deviceKey?: DeviceKey;
userKey?: UserKey;
masterKey?: MasterKey;
masterKeyEncryptedUserKey?: string;
deviceKey?: ReturnType<SymmetricCryptoKey["toJSON"]>;
organizationKeys?: EncryptionPair<
{ [orgId: string]: EncryptedOrganizationKeyData },
Record<string, SymmetricCryptoKey>
@@ -123,6 +122,18 @@ export class AccountKeys {
publicKey?: Uint8Array;
apiKeyClientSecret?: string;
/** @deprecated July 2023, left for migration purposes*/
cryptoMasterKey?: SymmetricCryptoKey;
/** @deprecated July 2023, left for migration purposes*/
cryptoMasterKeyAuto?: string;
/** @deprecated July 2023, left for migration purposes*/
cryptoMasterKeyBiometric?: string;
/** @deprecated July 2023, left for migration purposes*/
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
string,
SymmetricCryptoKey
>();
toJSON() {
return Utils.merge(this, {
publicKey: Utils.fromBufferToByteString(this.publicKey),
@@ -135,6 +146,9 @@ export class AccountKeys {
}
return Object.assign(new AccountKeys(), {
userKey: SymmetricCryptoKey.fromJSON(obj?.userKey),
masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey),
deviceKey: obj?.deviceKey,
cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey),
cryptoSymmetricKey: EncryptionPair.fromJSON(
obj?.cryptoSymmetricKey,
@@ -173,6 +187,7 @@ export class AccountProfile {
emailVerified?: boolean;
entityId?: string;
entityType?: string;
everHadUserKey?: boolean;
everBeenUnlocked?: boolean;
forcePasswordResetReason?: ForceResetPasswordReason;
hasPremiumPersonally?: boolean;
@@ -223,7 +238,8 @@ export class AccountSettings {
passwordGenerationOptions?: any;
usernameGenerationOptions?: any;
generatorOptions?: any;
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
pinKeyEncryptedUserKey?: EncryptedString;
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
protectedPin?: string;
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number;
@@ -234,6 +250,10 @@ export class AccountSettings {
activateAutoFillOnPageLoadFromPolicy?: boolean;
region?: string;
smOnboardingTasks?: Record<string, Record<string, boolean>>;
trustDeviceChoiceForDecryption?: boolean;
/** @deprecated July 2023, left for migration purposes*/
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
if (obj == null) {
@@ -269,12 +289,106 @@ export class AccountTokens {
}
}
export class AccountDecryptionOptions {
hasMasterPassword: boolean;
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
keyConnectorOption?: KeyConnectorUserDecryptionOption;
constructor(init?: Partial<AccountDecryptionOptions>) {
if (init) {
Object.assign(this, init);
}
}
// TODO: these nice getters don't work because the Account object is not properly being deserialized out of
// JSON (the Account static fromJSON method is not running) so these getters don't exist on the
// account decryptions options object when pulled out of state. This is a bug that needs to be fixed later on
// get hasTrustedDeviceOption(): boolean {
// return this.trustedDeviceOption !== null && this.trustedDeviceOption !== undefined;
// }
// get hasKeyConnectorOption(): boolean {
// return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined;
// }
static fromResponse(response: UserDecryptionOptionsResponse): AccountDecryptionOptions {
if (response == null) {
return null;
}
const accountDecryptionOptions = new AccountDecryptionOptions();
accountDecryptionOptions.hasMasterPassword = response.hasMasterPassword;
if (response.trustedDeviceOption) {
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
response.trustedDeviceOption.hasAdminApproval,
response.trustedDeviceOption.hasLoginApprovingDevice,
response.trustedDeviceOption.hasManageResetPasswordPermission
);
}
if (response.keyConnectorOption) {
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
response.keyConnectorOption.keyConnectorUrl
);
}
return accountDecryptionOptions;
}
static fromJSON(obj: Jsonify<AccountDecryptionOptions>): AccountDecryptionOptions {
if (obj == null) {
return null;
}
const accountDecryptionOptions = Object.assign(new AccountDecryptionOptions(), obj);
if (obj.trustedDeviceOption) {
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
obj.trustedDeviceOption.hasAdminApproval,
obj.trustedDeviceOption.hasLoginApprovingDevice,
obj.trustedDeviceOption.hasManageResetPasswordPermission
);
}
if (obj.keyConnectorOption) {
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
obj.keyConnectorOption.keyConnectorUrl
);
}
return accountDecryptionOptions;
}
}
export class LoginState {
ssoOrganizationIdentifier?: string;
constructor(init?: Partial<LoginState>) {
if (init) {
Object.assign(this, init);
}
}
static fromJSON(obj: Jsonify<LoginState>): LoginState {
if (obj == null) {
return null;
}
const loginState = Object.assign(new LoginState(), obj);
return loginState;
}
}
export class Account {
data?: AccountData = new AccountData();
keys?: AccountKeys = new AccountKeys();
profile?: AccountProfile = new AccountProfile();
settings?: AccountSettings = new AccountSettings();
tokens?: AccountTokens = new AccountTokens();
decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions();
loginState?: LoginState = new LoginState();
adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null;
constructor(init: Partial<Account>) {
Object.assign(this, {
@@ -298,6 +412,15 @@ export class Account {
...new AccountTokens(),
...init?.tokens,
},
decryptionOptions: {
...new AccountDecryptionOptions(),
...init?.decryptionOptions,
},
loginState: {
...new LoginState(),
...init?.loginState,
},
adminAuthRequest: init?.adminAuthRequest,
});
}
@@ -311,6 +434,9 @@ export class Account {
profile: AccountProfile.fromJSON(json?.profile),
settings: AccountSettings.fromJSON(json?.settings),
tokens: AccountTokens.fromJSON(json?.tokens),
decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions),
loginState: LoginState.fromJSON(json?.loginState),
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
});
}
}

View File

@@ -4,7 +4,11 @@ import { mock, MockProxy } from "jest-mock-extended";
import { EncryptionType } from "../../../enums";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import {
OrgKey,
SymmetricCryptoKey,
UserKey,
} from "../../../platform/models/domain/symmetric-crypto-key";
import { CryptoService } from "../../abstractions/crypto.service";
import { ContainerService } from "../../services/container.service";
@@ -225,12 +229,12 @@ describe("EncString", () => {
await encString.decrypt(null, key);
expect(cryptoService.getKeyForUserEncryption).not.toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
});
it("gets an organization key if required", async () => {
const orgKey = mock<SymmetricCryptoKey>();
const orgKey = mock<OrgKey>();
cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
@@ -241,13 +245,13 @@ describe("EncString", () => {
});
it("gets the user's decryption key if required", async () => {
const userKey = mock<SymmetricCryptoKey>();
const userKey = mock<UserKey>();
cryptoService.getKeyForUserEncryption.mockResolvedValue(userKey);
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
await encString.decrypt(null, null);
expect(cryptoService.getKeyForUserEncryption).toHaveBeenCalledWith();
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, userKey);
});
});

View File

@@ -1,4 +1,4 @@
import { Jsonify } from "type-fest";
import { Jsonify, Opaque } from "type-fest";
import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../enums";
import { Utils } from "../../../platform/misc/utils";
@@ -7,7 +7,7 @@ import { Encrypted } from "../../interfaces/encrypted";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
export class EncString implements Encrypted {
encryptedString?: string;
encryptedString?: EncryptedString;
encryptionType?: EncryptionType;
decryptedValue?: string;
data?: string;
@@ -53,14 +53,14 @@ export class EncString implements Encrypted {
private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) {
if (iv != null) {
this.encryptedString = encType + "." + iv + "|" + data;
this.encryptedString = (encType + "." + iv + "|" + data) as EncryptedString;
} else {
this.encryptedString = encType + "." + data;
this.encryptedString = (encType + "." + data) as EncryptedString;
}
// mac
if (mac != null) {
this.encryptedString += "|" + mac;
this.encryptedString = (this.encryptedString + "|" + mac) as EncryptedString;
}
this.encryptionType = encType;
@@ -70,7 +70,7 @@ export class EncString implements Encrypted {
}
private initFromEncryptedString(encryptedString: string) {
this.encryptedString = encryptedString as string;
this.encryptedString = encryptedString as EncryptedString;
if (!this.encryptedString) {
return;
}
@@ -162,6 +162,8 @@ export class EncString implements Encrypted {
const cryptoService = Utils.getContainerService().getCryptoService();
return orgId != null
? await cryptoService.getOrgKey(orgId)
: await cryptoService.getKeyForUserEncryption();
: await cryptoService.getUserKeyWithLegacySupport();
}
}
export type EncryptedString = Opaque<string, "EncString">;

View File

@@ -78,3 +78,8 @@ export class SymmetricCryptoKey {
// Setup all separate key types as opaque types
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
export type UserKey = Opaque<SymmetricCryptoKey, "UserKey">;
export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">;
export type ProviderKey = Opaque<SymmetricCryptoKey, "ProviderKey">;

View File

@@ -36,16 +36,25 @@ export class ConfigService implements ConfigServiceAbstraction {
try {
const response = await this.configApiService.get();
if (response != null) {
const data = new ServerConfigData(response);
const serverConfig = new ServerConfig(data);
this._serverConfig.next(serverConfig);
if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) {
return serverConfig;
}
if (response == null) {
return;
}
const data = new ServerConfigData(response);
const serverConfig = new ServerConfig(data);
this._serverConfig.next(serverConfig);
const userAuthStatus = await this.authService.getAuthStatus();
if (userAuthStatus !== AuthenticationStatus.LoggedOut) {
// Store the config for offline use if the user is logged in
await this.stateService.setServerConfig(data);
this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion);
}
// Always return new server config from server to calling method
// to ensure up to date information
// This change is specifically for the getFeatureFlag > buildServerConfig flow
// for locked or logged in users.
return serverConfig;
} catch {
return null;
}

View File

@@ -1,10 +1,18 @@
import { mock, mockReset } from "jest-mock-extended";
import { CsprngArray } from "../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { LogService } from "../abstractions/log.service";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { StateService } from "../abstractions/state.service";
import { EncString } from "../models/domain/enc-string";
import {
MasterKey,
PinKey,
SymmetricCryptoKey,
UserKey,
} from "../models/domain/symmetric-crypto-key";
import { CryptoService } from "../services/crypto.service";
describe("cryptoService", () => {
@@ -16,6 +24,8 @@ describe("cryptoService", () => {
const logService = mock<LogService>();
const stateService = mock<StateService>();
const mockUserId = "mock user id";
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
@@ -35,4 +45,172 @@ describe("cryptoService", () => {
it("instantiates", () => {
expect(cryptoService).not.toBeFalsy();
});
describe("getUserKey", () => {
let mockUserKey: UserKey;
let stateSvcGetUserKey: jest.SpyInstance;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
stateSvcGetUserKey = jest.spyOn(stateService, "getUserKey");
});
it("returns the User Key if available", async () => {
stateSvcGetUserKey.mockResolvedValue(mockUserKey);
const userKey = await cryptoService.getUserKey(mockUserId);
expect(stateSvcGetUserKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(userKey).toEqual(mockUserKey);
});
it("sets the Auto key if the User Key if not set", async () => {
const autoKeyB64 =
"IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g==";
stateService.getUserKeyAutoUnlock.mockResolvedValue(autoKeyB64);
const userKey = await cryptoService.getUserKey(mockUserId);
expect(stateService.setUserKey).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), {
userId: mockUserId,
});
expect(userKey.keyB64).toEqual(autoKeyB64);
});
});
describe("getUserKeyWithLegacySupport", () => {
let mockUserKey: UserKey;
let mockMasterKey: MasterKey;
let stateSvcGetUserKey: jest.SpyInstance;
let stateSvcGetMasterKey: jest.SpyInstance;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
stateSvcGetUserKey = jest.spyOn(stateService, "getUserKey");
stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey");
});
it("returns the User Key if available", async () => {
stateSvcGetUserKey.mockResolvedValue(mockUserKey);
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(stateSvcGetUserKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(stateSvcGetMasterKey).not.toHaveBeenCalled();
expect(userKey).toEqual(mockUserKey);
});
it("returns the user's master key when User Key is not available", async () => {
stateSvcGetUserKey.mockResolvedValue(null);
stateSvcGetMasterKey.mockResolvedValue(mockMasterKey);
const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId);
expect(stateSvcGetMasterKey).toHaveBeenCalledWith({ userId: mockUserId });
expect(userKey).toEqual(mockMasterKey);
});
});
describe("setUserKey", () => {
let mockUserKey: UserKey;
beforeEach(() => {
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
});
describe("Auto Key refresh", () => {
it("sets an Auto key if vault timeout is set to null", async () => {
stateService.getVaultTimeout.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(mockUserKey.keyB64, {
userId: mockUserId,
});
});
it("clears the Auto key if vault timeout is set to anything other than null", async () => {
stateService.getVaultTimeout.mockResolvedValue(10);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
it("clears the old deprecated Auto key whenever a User Key is set", async () => {
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setCryptoMasterKeyAuto).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
});
describe("Pin Key refresh", () => {
let cryptoSvcMakePinKey: jest.SpyInstance;
const protectedPin =
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=";
let encPin: EncString;
beforeEach(() => {
cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey");
cryptoSvcMakePinKey.mockResolvedValue(new SymmetricCryptoKey(new Uint8Array(64)) as PinKey);
encPin = new EncString(
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg="
);
encryptService.encrypt.mockResolvedValue(encPin);
});
it("sets a UserKeyPin if a ProtectedPin and UserKeyPin is set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(
new EncString(
"2.OdGNE3L23GaDZGvu9h2Brw==|/OAcNnrYwu0rjiv8+RUr3Tc+Ef8fV035Tm1rbTxfEuC+2LZtiCAoIvHIZCrM/V1PWnb/pHO2gh9+Koks04YhX8K29ED4FzjeYP8+YQD/dWo=|+12xTcIK/UVRsOyawYudPMHb6+lCHeR2Peq1pQhPm0A="
)
);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(expect.any(EncString), {
userId: mockUserId,
});
});
it("sets a PinKeyEphemeral if a ProtectedPin is set, but a UserKeyPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(
expect.any(EncString),
{
userId: mockUserId,
}
);
});
it("clears the UserKeyPin and UserKeyPinEphemeral if the ProtectedPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(null, {
userId: mockUserId,
});
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import { OrganizationData } from "../../admin-console/models/data/organization.d
import { PolicyData } from "../../admin-console/models/data/policy.data";
import { ProviderData } from "../../admin-console/models/data/provider.data";
import { Policy } from "../../admin-console/models/domain/policy";
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../../auth/models/domain/kdf-config";
@@ -43,6 +44,7 @@ import { ServerConfigData } from "../models/data/server-config.data";
import {
Account,
AccountData,
AccountDecryptionOptions,
AccountSettings,
AccountSettingsSettings,
} from "../models/domain/account";
@@ -50,7 +52,12 @@ import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state";
import { State } from "../models/domain/state";
import { StorageOptions } from "../models/domain/storage-options";
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import {
DeviceKey,
MasterKey,
SymmetricCryptoKey,
UserKey,
} from "../models/domain/symmetric-crypto-key";
const keys = {
state: "state",
@@ -62,6 +69,9 @@ const keys = {
};
const partialKeys = {
userAutoKey: "_user_auto",
userBiometricKey: "_user_biometric",
autoKey: "_masterkey_auto",
biometricKey: "_masterkey_biometric",
masterKey: "_masterkey",
@@ -112,7 +122,7 @@ export class StateService<
// FIXME: This should be refactored into AuthService or a similar service,
// as checking for the existence of the crypto key is a low level
// implementation detail.
this.activeAccountUnlockedSubject.next((await this.getCryptoMasterKey()) != null);
this.activeAccountUnlockedSubject.next((await this.getUserKey()) != null);
})
)
.subscribe();
@@ -515,6 +525,9 @@ export class StateService<
);
}
/**
* @deprecated Do not save the Master Key. Use the User Symmetric Key instead
*/
async getCryptoMasterKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -522,6 +535,9 @@ export class StateService<
return account?.keys?.cryptoMasterKey;
}
/**
* @deprecated Do not save the Master Key. Use the User Symmetric Key instead
*/
async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -542,6 +558,203 @@ export class StateService<
}
}
/**
* user key used to encrypt/decrypt data
*/
async getUserKey(options?: StorageOptions): Promise<UserKey> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return account?.keys?.userKey as UserKey;
}
/**
* user key used to encrypt/decrypt data
*/
async setUserKey(value: UserKey, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.userKey = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
if (options?.userId == this.activeAccountSubject.getValue()) {
const nextValue = value != null;
// Avoid emitting if we are already unlocked
if (this.activeAccountUnlockedSubject.getValue() != nextValue) {
this.activeAccountUnlockedSubject.next(nextValue);
}
}
}
/**
* User's master key derived from MP, saved only if we decrypted with MP
*/
async getMasterKey(options?: StorageOptions): Promise<MasterKey> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
return account?.keys?.masterKey;
}
/**
* User's master key derived from MP, saved only if we decrypted with MP
*/
async setMasterKey(value: MasterKey, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.keys.masterKey = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
/**
* The master key encrypted User symmetric key, saved on every auth
* so we can unlock with MP offline
*/
async getMasterKeyEncryptedUserKey(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.keys.masterKeyEncryptedUserKey;
}
/**
* The master key encrypted User symmetric key, saved on every auth
* so we can unlock with MP offline
*/
async setMasterKeyEncryptedUserKey(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.keys.masterKeyEncryptedUserKey = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
/**
* user key when using the "never" option of vault timeout
*/
async getUserKeyAutoUnlock(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}${partialKeys.userAutoKey}`,
options
);
}
/**
* user key when using the "never" option of vault timeout
*/
async setUserKeyAutoUnlock(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return;
}
await this.saveSecureStorageKey(partialKeys.userAutoKey, value, options);
}
/**
* User's encrypted symmetric key when using biometrics
*/
async getUserKeyBiometric(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(
`${options.userId}${partialKeys.userBiometricKey}`,
options
);
}
async hasUserKeyBiometric(options?: StorageOptions): Promise<boolean> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return false;
}
return await this.secureStorageService.has(
`${options.userId}${partialKeys.userBiometricKey}`,
options
);
}
async setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
await this.defaultSecureStorageOptions()
);
if (options?.userId == null) {
return;
}
await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options);
}
async getPinKeyEncryptedUserKey(options?: StorageOptions): Promise<EncString> {
return EncString.fromJSON(
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.settings?.pinKeyEncryptedUserKey
);
}
async setPinKeyEncryptedUserKey(value: EncString, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.settings.pinKeyEncryptedUserKey = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getPinKeyEncryptedUserKeyEphemeral(options?: StorageOptions): Promise<EncString> {
return EncString.fromJSON(
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
?.settings?.pinKeyEncryptedUserKeyEphemeral
);
}
async setPinKeyEncryptedUserKeyEphemeral(
value: EncString,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.settings.pinKeyEncryptedUserKeyEphemeral = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
/**
* @deprecated Use UserKeyAuto instead
*/
async getCryptoMasterKeyAuto(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }),
@@ -556,6 +769,9 @@ export class StateService<
);
}
/**
* @deprecated Use UserKeyAuto instead
*/
async setCryptoMasterKeyAuto(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "auto" }),
@@ -567,6 +783,9 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.autoKey, value, options);
}
/**
* @deprecated I don't see where this is even used
*/
async getCryptoMasterKeyB64(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
@@ -578,6 +797,9 @@ export class StateService<
);
}
/**
* @deprecated I don't see where this is even used
*/
async setCryptoMasterKeyB64(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
@@ -586,6 +808,9 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.masterKey, value, options);
}
/**
* @deprecated Use UserKeyBiometric instead
*/
async getCryptoMasterKeyBiometric(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
@@ -600,6 +825,9 @@ export class StateService<
);
}
/**
* @deprecated Use UserKeyBiometric instead
*/
async hasCryptoMasterKeyBiometric(options?: StorageOptions): Promise<boolean> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
@@ -614,6 +842,9 @@ export class StateService<
);
}
/**
* @deprecated Use UserKeyBiometric instead
*/
async setCryptoMasterKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(
this.reconcileOptions(options, { keySuffix: "biometric" }),
@@ -661,6 +892,9 @@ export class StateService<
);
}
/**
* @deprecated Use UserKey instead
*/
async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise<SymmetricCryptoKey> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -668,6 +902,9 @@ export class StateService<
return account?.keys?.cryptoSymmetricKey?.decrypted;
}
/**
* @deprecated Use UserKey instead
*/
async setDecryptedCryptoSymmetricKey(
value: SymmetricCryptoKey,
options?: StorageOptions
@@ -728,12 +965,18 @@ export class StateService<
);
}
/**
* @deprecated Use getPinKeyEncryptedUserKeyEphemeral instead
*/
async getDecryptedPinProtected(options?: StorageOptions): Promise<EncString> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.settings?.pinProtected?.decrypted;
}
/**
* @deprecated Use setPinKeyEncryptedUserKeyEphemeral instead
*/
async setDecryptedPinProtected(value: EncString, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
@@ -1069,10 +1312,17 @@ export class StateService<
const account = await this.getAccount(options);
return account?.keys?.deviceKey as DeviceKey;
const existingDeviceKey = account?.keys?.deviceKey;
// Must manually instantiate the SymmetricCryptoKey class from the JSON object
if (existingDeviceKey != null) {
return SymmetricCryptoKey.fromJSON(existingDeviceKey) as DeviceKey;
} else {
return null;
}
}
async setDeviceKey(value: DeviceKey, options?: StorageOptions): Promise<void> {
async setDeviceKey(value: DeviceKey | null, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
@@ -1081,7 +1331,94 @@ export class StateService<
const account = await this.getAccount(options);
account.keys.deviceKey = value;
account.keys.deviceKey = value?.toJSON() ?? null;
await this.saveAccount(account, options);
}
async getAdminAuthRequest(options?: StorageOptions): Promise<AdminAuthRequestStorable | null> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return null;
}
const account = await this.getAccount(options);
return account?.adminAuthRequest
? AdminAuthRequestStorable.fromJSON(account.adminAuthRequest)
: null;
}
async setAdminAuthRequest(
adminAuthRequest: AdminAuthRequestStorable,
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return;
}
const account = await this.getAccount(options);
account.adminAuthRequest = adminAuthRequest?.toJSON();
await this.saveAccount(account, options);
}
async getShouldTrustDevice(options?: StorageOptions): Promise<boolean | null> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return null;
}
const account = await this.getAccount(options);
return account?.settings?.trustDeviceChoiceForDecryption ?? null;
}
async setShouldTrustDevice(value: boolean, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return;
}
const account = await this.getAccount(options);
account.settings.trustDeviceChoiceForDecryption = value;
await this.saveAccount(account, options);
}
async getAccountDecryptionOptions(
options?: StorageOptions
): Promise<AccountDecryptionOptions | null> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return null;
}
const account = await this.getAccount(options);
return account?.decryptionOptions as AccountDecryptionOptions;
}
async setAccountDecryptionOptions(
value: AccountDecryptionOptions,
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
if (options?.userId == null) {
return;
}
const account = await this.getAccount(options);
account.decryptionOptions = value;
await this.saveAccount(account, options);
}
@@ -1366,12 +1703,18 @@ export class StateService<
);
}
/**
* @deprecated Use UserKey instead
*/
async getEncryptedCryptoSymmetricKey(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.keys.cryptoSymmetricKey.encrypted;
}
/**
* @deprecated Use UserKey instead
*/
async setEncryptedCryptoSymmetricKey(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
@@ -1655,6 +1998,24 @@ export class StateService<
);
}
async getEverHadUserKey(options?: StorageOptions): Promise<boolean> {
return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.profile?.everHadUserKey ?? false
);
}
async setEverHadUserKey(value: boolean, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.profile.everHadUserKey = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getEverBeenUnlocked(options?: StorageOptions): Promise<boolean> {
return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
@@ -2229,6 +2590,26 @@ export class StateService<
);
}
async getUserSsoOrganizationIdentifier(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.loginState?.ssoOrganizationIdentifier;
}
async setUserSsoOrganizationIdentifier(
value: string | null,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
account.loginState.ssoOrganizationIdentifier = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getTheme(options?: StorageOptions): Promise<ThemeType> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
@@ -2783,6 +3164,8 @@ export class StateService<
protected async removeAccountFromSecureStorage(userId: string = null): Promise<void> {
userId = userId ?? (await this.state())?.activeUserId;
await this.setUserKeyAutoUnlock(null, { userId: userId });
await this.setUserKeyBiometric(null, { userId: userId });
await this.setCryptoMasterKeyAuto(null, { userId: userId });
await this.setCryptoMasterKeyBiometric(null, { userId: userId });
await this.setCryptoMasterKeyB64(null, { userId: userId });
@@ -2808,11 +3191,12 @@ export class StateService<
}
}
// settings persist even on reset, and are not effected by this method
// settings persist even on reset, and are not affected by this method
protected resetAccount(account: TAccount) {
const persistentAccountInformation = {
settings: account.settings,
keys: { deviceKey: account.keys.deviceKey },
adminAuthRequest: account.adminAuthRequest,
};
return Object.assign(this.createAccount(), persistentAccountInformation);
}
@@ -2940,7 +3324,7 @@ export class StateService<
}
}
private deleteDiskCache(key: string) {
protected deleteDiskCache(key: string) {
if (this.useAccountCache) {
delete this.accountDiskCache.value[key];
this.accountDiskCache.next(this.accountDiskCache.value);

View File

@@ -39,8 +39,8 @@ export class SystemService implements SystemServiceAbstraction {
}
// User has set a PIN, with ask for master password on restart, to protect their vault
const decryptedPinProtected = await this.stateService.getDecryptedPinProtected();
if (decryptedPinProtected != null) {
const ephemeralPin = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
if (ephemeralPin != null) {
return;
}

View File

@@ -251,10 +251,15 @@ export class ApiService implements ApiServiceAbstraction {
}
}
// TODO: PM-3519: Create and move to AuthRequest Api service
async postAuthRequest(request: PasswordlessCreateAuthRequest): Promise<AuthRequestResponse> {
const r = await this.send("POST", "/auth-requests/", request, false, true);
return new AuthRequestResponse(r);
}
async postAdminAuthRequest(request: PasswordlessCreateAuthRequest): Promise<AuthRequestResponse> {
const r = await this.send("POST", "/auth-requests/admin-request", request, true, true);
return new AuthRequestResponse(r);
}
async getAuthResponse(id: string, accessCode: string): Promise<AuthRequestResponse> {
const path = `/auth-requests/${id}/response?code=${accessCode}`;
@@ -1580,7 +1585,9 @@ export class ApiService implements ApiServiceAbstraction {
// Key Connector
async getUserKeyFromKeyConnector(keyConnectorUrl: string): Promise<KeyConnectorUserKeyResponse> {
async getMasterKeyFromKeyConnector(
keyConnectorUrl: string
): Promise<KeyConnectorUserKeyResponse> {
const authHeader = await this.getActiveBearerToken();
const response = await this.fetch(

View File

@@ -1,85 +0,0 @@
import { DeviceCryptoServiceAbstraction } from "../abstractions/device-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
import { AppIdService } from "../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../platform/abstractions/crypto-function.service";
import { CryptoService } from "../platform/abstractions/crypto.service";
import { EncryptService } from "../platform/abstractions/encrypt.service";
import { StateService } from "../platform/abstractions/state.service";
import { SymmetricCryptoKey, DeviceKey } from "../platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "../types/csprng";
export class DeviceCryptoService implements DeviceCryptoServiceAbstraction {
constructor(
protected cryptoFunctionService: CryptoFunctionService,
protected cryptoService: CryptoService,
protected encryptService: EncryptService,
protected stateService: StateService,
protected appIdService: AppIdService,
protected devicesApiService: DevicesApiServiceAbstraction
) {}
async trustDevice(): Promise<DeviceResponse> {
// Attempt to get user symmetric key
const userSymKey: SymmetricCryptoKey = await this.cryptoService.getEncKey();
// If user symmetric key is not found, throw error
if (!userSymKey) {
throw new Error("User symmetric key not found");
}
// Generate deviceKey
const deviceKey = await this.makeDeviceKey();
// Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey
const [devicePublicKey, devicePrivateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(
2048
);
const [
devicePublicKeyEncryptedUserSymKey,
userSymKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey,
] = await Promise.all([
// Encrypt user symmetric key with the DevicePublicKey
this.cryptoService.rsaEncrypt(userSymKey.encKey, devicePublicKey),
// Encrypt devicePublicKey with user symmetric key
this.encryptService.encrypt(devicePublicKey, userSymKey),
// Encrypt devicePrivateKey with deviceKey
this.encryptService.encrypt(devicePrivateKey, deviceKey),
]);
// Send encrypted keys to server
const deviceIdentifier = await this.appIdService.getAppId();
return this.devicesApiService.updateTrustedDeviceKeys(
deviceIdentifier,
devicePublicKeyEncryptedUserSymKey.encryptedString,
userSymKeyEncryptedDevicePublicKey.encryptedString,
deviceKeyEncryptedDevicePrivateKey.encryptedString
);
}
async getDeviceKey(): Promise<DeviceKey> {
// Check if device key is already stored
const existingDeviceKey = await this.stateService.getDeviceKey();
if (existingDeviceKey != null) {
return existingDeviceKey;
} else {
return this.makeDeviceKey();
}
}
private async makeDeviceKey(): Promise<DeviceKey> {
// Create 512-bit device key
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
// Save device key in secure storage
await this.stateService.setDeviceKey(deviceKey);
return deviceKey;
}
}

View File

@@ -1,317 +0,0 @@
import { mock, mockReset } from "jest-mock-extended";
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
import { EncryptionType } from "../enums/encryption-type.enum";
import { AppIdService } from "../platform/abstractions/app-id.service";
import { CryptoFunctionService } from "../platform/abstractions/crypto-function.service";
import { EncryptService } from "../platform/abstractions/encrypt.service";
import { StateService } from "../platform/abstractions/state.service";
import { EncString } from "../platform/models/domain/enc-string";
import { SymmetricCryptoKey, DeviceKey } from "../platform/models/domain/symmetric-crypto-key";
import { CryptoService } from "../platform/services/crypto.service";
import { CsprngArray } from "../types/csprng";
import { DeviceCryptoService } from "./device-crypto.service.implementation";
describe("deviceCryptoService", () => {
let deviceCryptoService: DeviceCryptoService;
const cryptoFunctionService = mock<CryptoFunctionService>();
const cryptoService = mock<CryptoService>();
const encryptService = mock<EncryptService>();
const stateService = mock<StateService>();
const appIdService = mock<AppIdService>();
const devicesApiService = mock<DevicesApiServiceAbstraction>();
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(encryptService);
mockReset(stateService);
mockReset(appIdService);
mockReset(devicesApiService);
deviceCryptoService = new DeviceCryptoService(
cryptoFunctionService,
cryptoService,
encryptService,
stateService,
appIdService,
devicesApiService
);
});
it("instantiates", () => {
expect(deviceCryptoService).not.toBeFalsy();
});
describe("Trusted Device Encryption", () => {
const deviceKeyBytesLength = 64;
const userSymKeyBytesLength = 64;
describe("getDeviceKey", () => {
let mockRandomBytes: CsprngArray;
let mockDeviceKey: SymmetricCryptoKey;
let existingDeviceKey: DeviceKey;
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
let makeDeviceKeySpy: jest.SpyInstance;
beforeEach(() => {
mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes);
existingDeviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength) as CsprngArray
) as DeviceKey;
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
makeDeviceKeySpy = jest.spyOn(deviceCryptoService as any, "makeDeviceKey");
});
it("gets a device key when there is not an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
makeDeviceKeySpy.mockResolvedValue(mockDeviceKey);
const deviceKey = await deviceCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(deviceKey).toEqual(mockDeviceKey);
});
it("returns the existing device key without creating a new one when there is an existing device key", async () => {
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
const deviceKey = await deviceCryptoService.getDeviceKey();
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(makeDeviceKeySpy).not.toHaveBeenCalled();
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(deviceKey).toEqual(existingDeviceKey);
});
});
describe("makeDeviceKey", () => {
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
const cryptoFuncSvcRandomBytesSpy = jest
.spyOn(cryptoFunctionService, "randomBytes")
.mockResolvedValue(mockRandomBytes);
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
// TypeScript will allow calling private methods if the object is of type 'any'
// This is a hacky workaround, but it allows for cleaner tests
const deviceKey = await (deviceCryptoService as any).makeDeviceKey();
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
expect(deviceKey).not.toBeNull();
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
});
});
describe("trustDevice", () => {
let mockDeviceKeyRandomBytes: CsprngArray;
let mockDeviceKey: DeviceKey;
let mockUserSymKeyRandomBytes: CsprngArray;
let mockUserSymKey: SymmetricCryptoKey;
const deviceRsaKeyLength = 2048;
let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array];
let mockDevicePrivateKey: Uint8Array;
let mockDevicePublicKey: Uint8Array;
let mockDevicePublicKeyEncryptedUserSymKey: EncString;
let mockUserSymKeyEncryptedDevicePublicKey: EncString;
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
Id: "mockId",
Name: "mockName",
Identifier: "mockIdentifier",
Type: "mockType",
CreationDate: "mockCreationDate",
});
const mockDeviceId = "mockDeviceId";
let makeDeviceKeySpy: jest.SpyInstance;
let rsaGenerateKeyPairSpy: jest.SpyInstance;
let cryptoSvcGetEncKeySpy: jest.SpyInstance;
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
let encryptServiceEncryptSpy: jest.SpyInstance;
let appIdServiceGetAppIdSpy: jest.SpyInstance;
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
beforeEach(() => {
// Setup all spies and default return values for the happy path
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength) as CsprngArray;
mockUserSymKey = new SymmetricCryptoKey(mockUserSymKeyRandomBytes);
mockDeviceRsaKeyPair = [
new Uint8Array(deviceRsaKeyLength),
new Uint8Array(deviceRsaKeyLength),
];
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
mockDevicePublicKeyEncryptedUserSymKey = new EncString(
EncryptionType.Rsa2048_OaepSha1_B64,
"mockDevicePublicKeyEncryptedUserSymKey"
);
mockUserSymKeyEncryptedDevicePublicKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockUserSymKeyEncryptedDevicePublicKey"
);
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
EncryptionType.AesCbc256_HmacSha256_B64,
"mockDeviceKeyEncryptedDevicePrivateKey"
);
// TypeScript will allow calling private methods if the object is of type 'any'
makeDeviceKeySpy = jest
.spyOn(deviceCryptoService as any, "makeDeviceKey")
.mockResolvedValue(mockDeviceKey);
rsaGenerateKeyPairSpy = jest
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
.mockResolvedValue(mockDeviceRsaKeyPair);
cryptoSvcGetEncKeySpy = jest
.spyOn(cryptoService, "getEncKey")
.mockResolvedValue(mockUserSymKey);
cryptoSvcRsaEncryptSpy = jest
.spyOn(cryptoService, "rsaEncrypt")
.mockResolvedValue(mockDevicePublicKeyEncryptedUserSymKey);
encryptServiceEncryptSpy = jest
.spyOn(encryptService, "encrypt")
.mockImplementation((plainValue, key) => {
if (plainValue === mockDevicePublicKey && key === mockUserSymKey) {
return Promise.resolve(mockUserSymKeyEncryptedDevicePublicKey);
}
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
}
});
appIdServiceGetAppIdSpy = jest
.spyOn(appIdService, "getAppId")
.mockResolvedValue(mockDeviceId);
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
.mockResolvedValue(mockDeviceResponse);
});
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
const response = await deviceCryptoService.trustDevice();
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcGetEncKeySpy).toHaveBeenCalledTimes(1);
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
mockDeviceId,
mockDevicePublicKeyEncryptedUserSymKey.encryptedString,
mockUserSymKeyEncryptedDevicePublicKey.encryptedString,
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
);
expect(response).toBeInstanceOf(DeviceResponse);
expect(response).toEqual(mockDeviceResponse);
});
it("throws specific error if user symmetric key is not found", async () => {
// setup the spy to return null
cryptoSvcGetEncKeySpy.mockResolvedValue(null);
// check if the expected error is thrown
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
// reset the spy
cryptoSvcGetEncKeySpy.mockReset();
// setup the spy to return undefined
cryptoSvcGetEncKeySpy.mockResolvedValue(undefined);
// check if the expected error is thrown
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
"User symmetric key not found"
);
});
const methodsToTestForErrorsOrInvalidReturns = [
{
method: "makeDeviceKey",
spy: () => makeDeviceKeySpy,
errorText: "makeDeviceKey error",
},
{
method: "rsaGenerateKeyPair",
spy: () => rsaGenerateKeyPairSpy,
errorText: "rsaGenerateKeyPair error",
},
{
method: "getEncKey",
spy: () => cryptoSvcGetEncKeySpy,
errorText: "getEncKey error",
},
{
method: "rsaEncrypt",
spy: () => cryptoSvcRsaEncryptSpy,
errorText: "rsaEncrypt error",
},
{
method: "encryptService.encrypt",
spy: () => encryptServiceEncryptSpy,
errorText: "encryptService.encrypt error",
},
];
describe.each(methodsToTestForErrorsOrInvalidReturns)(
"trustDevice error handling and invalid return testing",
({ method, spy, errorText }) => {
// ensures that error propagation works correctly
it(`throws an error if ${method} fails`, async () => {
const methodSpy = spy();
methodSpy.mockRejectedValue(new Error(errorText));
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(errorText);
});
test.each([null, undefined])(
`throws an error if ${method} returns %s`,
async (invalidValue) => {
const methodSpy = spy();
methodSpy.mockResolvedValue(invalidValue);
await expect(deviceCryptoService.trustDevice()).rejects.toThrow();
}
);
}
);
});
});
});

View File

@@ -1,64 +0,0 @@
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { Utils } from "../../platform/misc/utils";
import { ApiService } from "../api.service";
import { TrustedDeviceKeysRequest } from "./requests/trusted-device-keys.request";
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
const r = await this.apiService.send(
"GET",
"/devices/knowndevice",
null,
false,
true,
null,
(headers) => {
headers.set("X-Device-Identifier", deviceIdentifier);
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
}
);
return r as boolean;
}
/**
* Get device by identifier
* @param deviceIdentifier - client generated id (not device id in DB)
*/
async getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse> {
const r = await this.apiService.send(
"GET",
`/devices/identifier/${deviceIdentifier}`,
null,
true,
true
);
return new DeviceResponse(r);
}
async updateTrustedDeviceKeys(
deviceIdentifier: string,
devicePublicKeyEncryptedUserSymKey: string,
userSymKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
): Promise<DeviceResponse> {
const request = new TrustedDeviceKeysRequest(
devicePublicKeyEncryptedUserSymKey,
userSymKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey
);
const result = await this.apiService.send(
"PUT",
`/devices/${deviceIdentifier}/keys`,
request,
true,
true
);
return new DeviceResponse(result);
}
}

View File

@@ -0,0 +1,68 @@
import { Observable, defer, map } from "rxjs";
import { DevicesServiceAbstraction } from "../../abstractions/devices/devices.service.abstraction";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { DeviceView } from "../../abstractions/devices/views/device.view";
import { DevicesApiServiceAbstraction } from "../../auth/abstractions/devices-api.service.abstraction";
import { ListResponse } from "../../models/response/list.response";
/**
* @class DevicesServiceImplementation
* @implements {DevicesServiceAbstraction}
* @description Observable based data store service for Devices.
* note: defer is used to convert the promises to observables and to ensure
* that observables are created for each subscription
* (i.e., promsise --> observables are cold until subscribed to)
*/
export class DevicesServiceImplementation implements DevicesServiceAbstraction {
constructor(private devicesApiService: DevicesApiServiceAbstraction) {}
/**
* @description Gets the list of all devices.
*/
getDevices$(): Observable<Array<DeviceView>> {
return defer(() => this.devicesApiService.getDevices()).pipe(
map((deviceResponses: ListResponse<DeviceResponse>) => {
return deviceResponses.data.map((deviceResponse: DeviceResponse) => {
return new DeviceView(deviceResponse);
});
})
);
}
/**
* @description Gets the device with the specified identifier.
*/
getDeviceByIdentifier$(deviceIdentifier: string): Observable<DeviceView> {
return defer(() => this.devicesApiService.getDeviceByIdentifier(deviceIdentifier)).pipe(
map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse))
);
}
/**
* @description Checks if a device is known for a user by user's email and device's identifier.
*/
isDeviceKnownForUser$(email: string, deviceIdentifier: string): Observable<boolean> {
return defer(() => this.devicesApiService.getKnownDevice(email, deviceIdentifier));
}
/**
* @description Updates the keys for the specified device.
*/
updateTrustedDeviceKeys$(
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
): Observable<DeviceView> {
return defer(() =>
this.devicesApiService.updateTrustedDeviceKeys(
deviceIdentifier,
devicePublicKeyEncryptedUserKey,
userKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey
)
).pipe(map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse)));
}
}

View File

@@ -0,0 +1,146 @@
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { Policy } from "../../admin-console/models/domain/policy";
import { TokenService } from "../../auth/abstractions/token.service";
import { UserVerificationService } from "../../auth/abstractions/user-verification/user-verification.service.abstraction";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncString } from "../../platform/models/domain/enc-string";
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
describe("VaultTimeoutSettingsService", () => {
let cryptoService: MockProxy<CryptoService>;
let tokenService: MockProxy<TokenService>;
let policyService: MockProxy<PolicyService>;
let stateService: MockProxy<StateService>;
let userVerificationService: MockProxy<UserVerificationService>;
let service: VaultTimeoutSettingsService;
beforeEach(() => {
cryptoService = mock<CryptoService>();
tokenService = mock<TokenService>();
policyService = mock<PolicyService>();
stateService = mock<StateService>();
userVerificationService = mock<UserVerificationService>();
service = new VaultTimeoutSettingsService(
cryptoService,
tokenService,
policyService,
stateService,
userVerificationService
);
});
describe("availableVaultTimeoutActions$", () => {
it("always returns LogOut", async () => {
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.LogOut);
});
it("contains Lock when the user has a master password", async () => {
userVerificationService.hasMasterPassword.mockResolvedValue(true);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has a persistent PIN configured", async () => {
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(createEncString());
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has a transient/ephemeral PIN configured", async () => {
stateService.getProtectedPin.mockResolvedValue("some-key");
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has biometrics configured", async () => {
stateService.getBiometricUnlock.mockResolvedValue(true);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => {
userVerificationService.hasMasterPassword.mockResolvedValue(false);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
stateService.getProtectedPin.mockResolvedValue(null);
stateService.getBiometricUnlock.mockResolvedValue(false);
const result = await firstValueFrom(service.availableVaultTimeoutActions$());
expect(result).not.toContain(VaultTimeoutAction.Lock);
});
});
describe("vaultTimeoutAction$", () => {
describe("given the user has a master password", () => {
it.each`
policy | userPreference | expected
${null} | ${null} | ${VaultTimeoutAction.Lock}
${null} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.LogOut}
${VaultTimeoutAction.LogOut} | ${null} | ${VaultTimeoutAction.LogOut}
${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut}
`(
"returns $expected when policy is $policy, and user preference is $userPreference",
async ({ policy, userPreference, expected }) => {
userVerificationService.hasMasterPassword.mockResolvedValue(true);
policyService.policyAppliesToUser.mockResolvedValue(policy === null ? false : true);
policyService.getAll.mockResolvedValue(
policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])
);
stateService.getVaultTimeoutAction.mockResolvedValue(userPreference);
const result = await firstValueFrom(service.vaultTimeoutAction$());
expect(result).toBe(expected);
}
);
});
describe("given the user does not have a master password", () => {
it.each`
unlockMethod | policy | userPreference | expected
${false} | ${null} | ${null} | ${VaultTimeoutAction.LogOut}
${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut}
${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.LogOut}
${true} | ${null} | ${null} | ${VaultTimeoutAction.LogOut}
${true} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock}
${true} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock}
${true} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock}
`(
"returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference",
async ({ unlockMethod, policy, userPreference, expected }) => {
stateService.getBiometricUnlock.mockResolvedValue(unlockMethod);
userVerificationService.hasMasterPassword.mockResolvedValue(false);
policyService.policyAppliesToUser.mockResolvedValue(policy === null ? false : true);
policyService.getAll.mockResolvedValue(
policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])
);
stateService.getVaultTimeoutAction.mockResolvedValue(userPreference);
const result = await firstValueFrom(service.vaultTimeoutAction$());
expect(result).toBe(expected);
}
);
});
});
});
function createEncString() {
return Symbol() as unknown as EncString;
}

View File

@@ -1,17 +1,28 @@
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { defer } from "rxjs";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../admin-console/enums";
import { TokenService } from "../../auth/abstractions/token.service";
import { UserVerificationService } from "../../auth/abstractions/user-verification/user-verification.service.abstraction";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { StateService } from "../../platform/abstractions/state.service";
/**
* - DISABLED: No Pin set
* - PERSISTENT: Pin is set and survives client reset
* - TRANSIENT: Pin is set and requires password unlock after client reset
*/
export type PinLockType = "DISABLED" | "PERSISTANT" | "TRANSIENT";
export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private tokenService: TokenService,
private policyService: PolicyService,
private stateService: StateService
private stateService: StateService,
private userVerificationService: UserVerificationService
) {}
async setVaultTimeoutOptions(timeout: number, action: VaultTimeoutAction): Promise<void> {
@@ -41,21 +52,35 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
await this.tokenService.setClientId(clientId);
await this.tokenService.setClientSecret(clientSecret);
await this.cryptoService.toggleKey();
await this.cryptoService.refreshAdditionalKeys();
}
async isPinLockSet(): Promise<[boolean, boolean]> {
const protectedPin = await this.stateService.getProtectedPin();
const pinProtectedKey = await this.stateService.getEncryptedPinProtected();
return [protectedPin != null, pinProtectedKey != null];
availableVaultTimeoutActions$(userId?: string) {
return defer(() => this.getAvailableVaultTimeoutActions(userId));
}
async isBiometricLockSet(): Promise<boolean> {
return await this.stateService.getBiometricUnlock();
async isPinLockSet(userId?: string): Promise<PinLockType> {
// we can't check the protected pin for both because old accounts only
// used it for MP on Restart
const pinIsEnabled = !!(await this.stateService.getProtectedPin({ userId }));
const aUserKeyPinIsSet = !!(await this.stateService.getPinKeyEncryptedUserKey({ userId }));
const anOldUserKeyPinIsSet = !!(await this.stateService.getEncryptedPinProtected({ userId }));
if (aUserKeyPinIsSet || anOldUserKeyPinIsSet) {
return "PERSISTANT";
} else if (pinIsEnabled && !aUserKeyPinIsSet && !anOldUserKeyPinIsSet) {
return "TRANSIENT";
} else {
return "DISABLED";
}
}
async isBiometricLockSet(userId?: string): Promise<boolean> {
return await this.stateService.getBiometricUnlock({ userId });
}
async getVaultTimeout(userId?: string): Promise<number> {
const vaultTimeout = await this.stateService.getVaultTimeout({ userId: userId });
const vaultTimeout = await this.stateService.getVaultTimeout({ userId });
if (
await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)
@@ -68,9 +93,11 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
timeout = policy[0].data.minutes;
}
// TODO @jlf0dev: Can we move this somwhere else? Maybe add it to the initialization process?
// ( Apparently I'm the one that reviewed the original PR that added this :) )
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (vaultTimeout !== timeout) {
await this.stateService.setVaultTimeout(timeout, { userId: userId });
await this.stateService.setVaultTimeout(timeout, { userId });
}
return timeout;
@@ -79,22 +106,40 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
return vaultTimeout;
}
vaultTimeoutAction$(userId?: string) {
return defer(() => this.getVaultTimeoutAction(userId));
}
async getVaultTimeoutAction(userId?: string): Promise<VaultTimeoutAction> {
let vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId });
const availableActions = await this.getAvailableVaultTimeoutActions();
if (availableActions.length === 1) {
return availableActions[0];
}
const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId: userId });
if (
await this.policyService.policyAppliesToUser(PolicyType.MaximumVaultTimeout, null, userId)
) {
const policy = await this.policyService.getAll(PolicyType.MaximumVaultTimeout, userId);
const action = policy[0].data.action;
if (action) {
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (action && vaultTimeoutAction !== action) {
await this.stateService.setVaultTimeoutAction(action, { userId: userId });
}
vaultTimeoutAction = action;
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
if (action && vaultTimeoutAction !== action) {
await this.stateService.setVaultTimeoutAction(action, { userId: userId });
}
if (action && availableActions.includes(action)) {
return action;
}
}
if (vaultTimeoutAction == null) {
// Depends on whether or not the user has a master password
const defaultValue = (await this.userVerificationService.hasMasterPassword())
? VaultTimeoutAction.Lock
: VaultTimeoutAction.LogOut;
// We really shouldn't need to set the value here, but multiple services relies on this value being correct.
await this.stateService.setVaultTimeoutAction(defaultValue, { userId: userId });
return defaultValue;
}
return vaultTimeoutAction === VaultTimeoutAction.LogOut
@@ -102,9 +147,23 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
: VaultTimeoutAction.Lock;
}
private async getAvailableVaultTimeoutActions(userId?: string): Promise<VaultTimeoutAction[]> {
const availableActions = [VaultTimeoutAction.LogOut];
const canLock =
(await this.userVerificationService.hasMasterPassword(userId)) ||
(await this.isPinLockSet(userId)) !== "DISABLED" ||
(await this.isBiometricLockSet(userId));
if (canLock) {
availableActions.push(VaultTimeoutAction.Lock);
}
return availableActions;
}
async clear(userId?: string): Promise<void> {
await this.stateService.setEverBeenUnlocked(false, { userId: userId });
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId });
await this.cryptoService.clearPinKeys(userId);
}
}

View File

@@ -1,10 +1,9 @@
import { firstValueFrom } from "rxjs";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { KeyConnectorService } from "../../auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
@@ -26,7 +25,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
protected platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private searchService: SearchService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
private authService: AuthService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
@@ -34,10 +32,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null
) {}
init(checkOnInterval: boolean) {
async init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3483)
await this.migrateKeyForNeverLockIfNeeded();
this.inited = true;
if (checkOnInterval) {
@@ -69,14 +69,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
return;
}
if (await this.keyConnectorService.getUsesKeyConnector()) {
const pinSet = await this.vaultTimeoutSettingsService.isPinLockSet();
const pinLock =
(pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) || pinSet[1];
if (!pinLock && !(await this.vaultTimeoutSettingsService.isBiometricLockSet())) {
await this.logOut(userId);
}
const availableActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$()
);
const supportsLock = availableActions.includes(VaultTimeoutAction.Lock);
if (!supportsLock) {
await this.logOut(userId);
}
if (userId == null || userId === (await this.stateService.getUserId())) {
@@ -85,12 +83,13 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
}
await this.stateService.setEverBeenUnlocked(true, { userId: userId });
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
await this.cryptoService.clearKey(false, userId);
await this.cryptoService.clearUserKey(false, userId);
await this.cryptoService.clearMasterKey(userId);
await this.cryptoService.clearOrgKeys(true, userId);
await this.cryptoService.clearKeyPair(true, userId);
await this.cryptoService.clearEncKey(true, userId);
await this.cipherService.clearCache(userId);
await this.collectionService.clearCache(userId);
@@ -133,9 +132,20 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
}
private async executeTimeoutAction(userId: string): Promise<void> {
const timeoutAction = await this.vaultTimeoutSettingsService.getVaultTimeoutAction(userId);
const timeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId)
);
timeoutAction === VaultTimeoutAction.LogOut
? await this.logOut(userId)
: await this.lock(userId);
}
private async migrateKeyForNeverLockIfNeeded(): Promise<void> {
const accounts = await firstValueFrom(this.stateService.accounts$);
for (const userId in accounts) {
if (userId != null) {
await this.cryptoService.migrateAutoKeyIfNeeded(userId);
}
}
}
}

View File

@@ -336,7 +336,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
}
async getHistory(): Promise<GeneratedPasswordHistory[]> {
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
return new Array<GeneratedPasswordHistory>();
}
@@ -356,7 +356,7 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
async addHistory(password: string): Promise<void> {
// Cannot add new history if no key is available
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
return;
}

View File

@@ -143,9 +143,9 @@ export class SendService implements InternalSendServiceAbstraction {
}
decSends = [];
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
throw new Error("No key.");
throw new Error("No user key found.");
}
const promises: Promise<any>[] = [];
@@ -249,7 +249,7 @@ export class SendService implements InternalSendServiceAbstraction {
const sends = Object.values(sendsMap || {}).map((f) => new Send(f));
this._sends.next(sends);
if (await this.cryptoService.hasKey()) {
if (await this.cryptoService.hasUserKey()) {
this._sendViews.next(await this.decryptSends(sends));
}
}

View File

@@ -3,8 +3,12 @@ import { mock, MockProxy } from "jest-mock-extended";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import {
OrgKey,
SymmetricCryptoKey,
UserKey,
} from "../../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../../platform/services/container.service";
import { AttachmentData } from "../../models/data/attachment.data";
import { Attachment } from "../../models/domain/attachment";
@@ -105,12 +109,12 @@ describe("Attachment", () => {
await attachment.decrypt(null, providedKey);
expect(cryptoService.getKeyForUserEncryption).not.toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey);
});
it("gets an organization key if required", async () => {
const orgKey = mock<SymmetricCryptoKey>();
const orgKey = mock<OrgKey>();
cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
await attachment.decrypt("orgId", null);
@@ -120,12 +124,12 @@ describe("Attachment", () => {
});
it("gets the user's decryption key if required", async () => {
const userKey = mock<SymmetricCryptoKey>();
cryptoService.getKeyForUserEncryption.mockResolvedValue(userKey);
const userKey = mock<UserKey>();
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
await attachment.decrypt(null, null);
expect(cryptoService.getKeyForUserEncryption).toHaveBeenCalled();
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalled();
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey);
});
});
@@ -136,8 +140,8 @@ describe("Attachment", () => {
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
const actual = Attachment.fromJSON({
key: "myKey",
fileName: "myFileName",
key: "myKey" as EncryptedString,
fileName: "myFileName" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -71,7 +71,7 @@ export class Attachment extends Domain {
const cryptoService = Utils.getContainerService().getCryptoService();
return orgId != null
? await cryptoService.getOrgKey(orgId)
: await cryptoService.getKeyForUserEncryption();
: await cryptoService.getUserKeyWithLegacySupport();
}
toAttachmentData(): AttachmentData {

View File

@@ -1,5 +1,5 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { CardData } from "../../../vault/models/data/card.data";
import { Card } from "../../models/domain/card";
@@ -76,12 +76,12 @@ describe("Card", () => {
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
const actual = Card.fromJSON({
cardholderName: "mockCardHolder",
brand: "mockBrand",
number: "mockNumber",
expMonth: "mockExpMonth",
expYear: "mockExpYear",
code: "mockCode",
cardholderName: "mockCardHolder" as EncryptedString,
brand: "mockBrand" as EncryptedString,
number: "mockNumber" as EncryptedString,
expMonth: "mockExpMonth" as EncryptedString,
expYear: "mockExpYear" as EncryptedString,
code: "mockCode" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -1,6 +1,6 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { FieldType } from "../../../enums";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { FieldData } from "../../models/data/field.data";
import { Field } from "../../models/domain/field";
@@ -67,8 +67,8 @@ describe("Field", () => {
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
const actual = Field.fromJSON({
name: "myName",
value: "myValue",
name: "myName" as EncryptedString,
value: "myValue" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -1,5 +1,5 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { FolderData } from "../../models/data/folder.data";
import { Folder } from "../../models/domain/folder";
@@ -47,7 +47,7 @@ describe("Folder", () => {
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
const actual = Folder.fromJSON({
revisionDate: revisionDate.toISOString(),
name: "name",
name: "name" as EncryptedString,
id: "id",
});

View File

@@ -1,5 +1,5 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { IdentityData } from "../../models/data/identity.data";
import { Identity } from "../../models/domain/identity";
@@ -137,24 +137,24 @@ describe("Identity", () => {
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
const actual = Identity.fromJSON({
firstName: "mockFirstName",
lastName: "mockLastName",
address1: "mockAddress1",
address2: "mockAddress2",
address3: "mockAddress3",
city: "mockCity",
company: "mockCompany",
country: "mockCountry",
email: "mockEmail",
licenseNumber: "mockLicenseNumber",
middleName: "mockMiddleName",
passportNumber: "mockPassportNumber",
phone: "mockPhone",
postalCode: "mockPostalCode",
ssn: "mockSsn",
state: "mockState",
title: "mockTitle",
username: "mockUsername",
firstName: "mockFirstName" as EncryptedString,
lastName: "mockLastName" as EncryptedString,
address1: "mockAddress1" as EncryptedString,
address2: "mockAddress2" as EncryptedString,
address3: "mockAddress3" as EncryptedString,
city: "mockCity" as EncryptedString,
company: "mockCompany" as EncryptedString,
country: "mockCountry" as EncryptedString,
email: "mockEmail" as EncryptedString,
licenseNumber: "mockLicenseNumber" as EncryptedString,
middleName: "mockMiddleName" as EncryptedString,
passportNumber: "mockPassportNumber" as EncryptedString,
phone: "mockPhone" as EncryptedString,
postalCode: "mockPostalCode" as EncryptedString,
ssn: "mockSsn" as EncryptedString,
state: "mockState" as EncryptedString,
title: "mockTitle" as EncryptedString,
username: "mockUsername" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -3,7 +3,7 @@ import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { mockEnc, mockFromJson } from "../../../../spec";
import { UriMatchType } from "../../../enums";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { LoginData } from "../../models/data/login.data";
import { Login } from "../../models/domain/login";
import { LoginUri } from "../../models/domain/login-uri";
@@ -108,10 +108,10 @@ describe("Login DTO", () => {
const actual = Login.fromJSON({
uris: ["loginUri1", "loginUri2"] as any,
username: "myUsername",
password: "myPassword",
username: "myUsername" as EncryptedString,
password: "myPassword" as EncryptedString,
passwordRevisionDate: passwordRevisionDate.toISOString(),
totp: "myTotp",
totp: "myTotp" as EncryptedString,
});
expect(actual).toEqual({

View File

@@ -1,5 +1,5 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { PasswordHistoryData } from "../../models/data/password-history.data";
import { Password } from "../../models/domain/password";
@@ -55,7 +55,7 @@ describe("Password", () => {
const lastUsedDate = new Date("2022-01-31T12:00:00.000Z");
const actual = Password.fromJSON({
password: "myPassword",
password: "myPassword" as EncryptedString,
lastUsedDate: lastUsedDate.toISOString(),
});

View File

@@ -9,7 +9,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { OrgKey, SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type";
@@ -118,11 +118,11 @@ describe("Cipher Service", () => {
describe("saveAttachmentRawWithServer()", () => {
it("should upload encrypted file contents with save attachments", async () => {
const fileName = "filename";
const fileData = new Uint8Array(10).buffer;
const fileData = new Uint8Array(10);
cryptoService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)))
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey)
);
cryptoService.makeEncKey.mockReturnValue(
cryptoService.makeDataEncKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)))
);
const spy = jest.spyOn(cipherFileUploadService, "upload");

View File

@@ -13,7 +13,11 @@ import { Utils } from "../../platform/misc/utils";
import Domain from "../../platform/models/domain/domain-base";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import {
OrgKey,
SymmetricCryptoKey,
UserKey,
} from "../../platform/models/domain/symmetric-crypto-key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { CipherType } from "../enums/cipher-type";
@@ -325,14 +329,14 @@ export class CipherService implements CipherServiceAbstraction {
return await this.getDecryptedCipherCache();
}
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
throw new Error("No key.");
throw new Error("No user key found.");
}
const ciphers = await this.getAll();
const orgKeys = await this.cryptoService.getOrgKeys();
const userKey = await this.cryptoService.getKeyForUserEncryption();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
// Group ciphers by orgId or under 'null' for the user's ciphers
const grouped = ciphers.reduce((agg, c) => {
@@ -636,14 +640,17 @@ export class CipherService implements CipherServiceAbstraction {
async saveAttachmentRawWithServer(
cipher: Cipher,
filename: string,
data: ArrayBuffer,
data: Uint8Array,
admin = false
): Promise<Cipher> {
const key = await this.cryptoService.getOrgKey(cipher.organizationId);
const encFileName = await this.cryptoService.encrypt(filename, key);
let encKey: UserKey | OrgKey;
encKey = await this.cryptoService.getOrgKey(cipher.organizationId);
encKey ||= await this.cryptoService.getUserKeyWithLegacySupport();
const dataEncKey = await this.cryptoService.makeEncKey(key);
const encData = await this.cryptoService.encryptToBytes(new Uint8Array(data), dataEncKey[0]);
const dataEncKey = await this.cryptoService.makeDataEncKey(encKey);
const encFileName = await this.encryptService.encrypt(filename, encKey);
const encData = await this.encryptService.encryptToBytes(data, dataEncKey[0]);
const response = await this.cipherFileUploadService.upload(
cipher,
@@ -970,11 +977,15 @@ export class CipherService implements CipherServiceAbstraction {
const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse);
const decBuf = await this.cryptoService.decryptFromBytes(encBuf, null);
const key = await this.cryptoService.getOrgKey(organizationId);
const encFileName = await this.cryptoService.encrypt(attachmentView.fileName, key);
const dataEncKey = await this.cryptoService.makeEncKey(key);
const encData = await this.cryptoService.encryptToBytes(decBuf, dataEncKey[0]);
let encKey: UserKey | OrgKey;
encKey = await this.cryptoService.getOrgKey(organizationId);
encKey ||= (await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey;
const dataEncKey = await this.cryptoService.makeDataEncKey(encKey);
const encFileName = await this.encryptService.encrypt(attachmentView.fileName, encKey);
const encData = await this.encryptService.encryptToBytes(new Uint8Array(decBuf), dataEncKey[0]);
const fd = new FormData();
try {

View File

@@ -79,7 +79,7 @@ export class CollectionService implements CollectionServiceAbstraction {
return decryptedCollections;
}
const hasKey = await this.cryptoService.hasKey();
const hasKey = await this.cryptoService.hasUserKey();
if (!hasKey) {
throw new Error("No key.");
}

View File

@@ -173,7 +173,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
this._folders.next(folders);
if (await this.cryptoService.hasKey()) {
if (await this.cryptoService.hasUserKey()) {
this._folderViews.next(await this.decryptFolders(folders));
}
}

View File

@@ -303,8 +303,8 @@ export class SyncService implements SyncServiceAbstraction {
throw new Error("Stamp has changed");
}
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(response.privateKey);
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
await this.cryptoService.setPrivateKey(response.privateKey);
await this.cryptoService.setProviderKeys(response.providers);
await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations);
await this.stateService.setAvatarColor(response.avatarColor);