1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-12 00:41:29 +01:00

Merge branch 'main' into autofill/pm-5189-fix-issues-present-with-inline-menu-rendering-in-iframes

This commit is contained in:
Cesar Gonzalez 2024-04-11 08:24:15 -05:00
commit f702d1f08a
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
163 changed files with 2582 additions and 1328 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/browser", "name": "@bitwarden/browser",
"version": "2024.3.1", "version": "2024.4.1",
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack",

View File

@ -3005,5 +3005,8 @@
}, },
"passkeyRemoved": { "passkeyRemoved": {
"message": "Passkey removed" "message": "Passkey removed"
},
"unassignedItemsBanner": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
} }
} }

View File

@ -95,10 +95,10 @@
"message": "Auto-fill login" "message": "Auto-fill login"
}, },
"autoFillCard": { "autoFillCard": {
"message": "Auto-fill card" "message": "Auto-bete txartela"
}, },
"autoFillIdentity": { "autoFillIdentity": {
"message": "Auto-fill identity" "message": "Auto-bete nortasuna"
}, },
"generatePasswordCopied": { "generatePasswordCopied": {
"message": "Sortu pasahitza (kopiatuta)" "message": "Sortu pasahitza (kopiatuta)"
@ -110,19 +110,19 @@
"message": "Bat datozen saio-hasierarik gabe" "message": "Bat datozen saio-hasierarik gabe"
}, },
"noCards": { "noCards": {
"message": "No cards" "message": "Txartelik ez"
}, },
"noIdentities": { "noIdentities": {
"message": "No identities" "message": "Nortasunik ez"
}, },
"addLoginMenu": { "addLoginMenu": {
"message": "Add login" "message": "Add login"
}, },
"addCardMenu": { "addCardMenu": {
"message": "Add card" "message": "Gehitu txartela"
}, },
"addIdentityMenu": { "addIdentityMenu": {
"message": "Add identity" "message": "Gehitu nortasuna"
}, },
"unlockVaultMenu": { "unlockVaultMenu": {
"message": "Desblokeatu kutxa gotorra" "message": "Desblokeatu kutxa gotorra"
@ -223,10 +223,10 @@
"message": "Bitwarden Laguntza zentroa" "message": "Bitwarden Laguntza zentroa"
}, },
"communityForums": { "communityForums": {
"message": "Explore Bitwarden community forums" "message": "Esploratu Bitwarden komunitatearen foroak"
}, },
"contactSupport": { "contactSupport": {
"message": "Contact Bitwarden support" "message": "Jarri harremanetan Bitwardeneko laguntza taldearekin"
}, },
"sync": { "sync": {
"message": "Sinkronizatu" "message": "Sinkronizatu"
@ -269,7 +269,7 @@
"message": "Luzera" "message": "Luzera"
}, },
"passwordMinLength": { "passwordMinLength": {
"message": "Minimum password length" "message": "Pasahitzaren gutxieneko luzera"
}, },
"uppercase": { "uppercase": {
"message": "Letra larria (A-Z)" "message": "Letra larria (A-Z)"
@ -1064,7 +1064,7 @@
"message": "Edit browser settings." "message": "Edit browser settings."
}, },
"autofillOverlayVisibilityOff": { "autofillOverlayVisibilityOff": {
"message": "Off", "message": "Itzalita",
"description": "Overlay setting select option for disabling autofill overlay" "description": "Overlay setting select option for disabling autofill overlay"
}, },
"autofillOverlayVisibilityOnFieldFocus": { "autofillOverlayVisibilityOnFieldFocus": {
@ -1592,10 +1592,10 @@
"message": "Ezarri pasahitz nagusia" "message": "Ezarri pasahitz nagusia"
}, },
"currentMasterPass": { "currentMasterPass": {
"message": "Current master password" "message": "Oraingo pasahitz nagusia"
}, },
"newMasterPass": { "newMasterPass": {
"message": "New master password" "message": "Pasahitz nagusi berria"
}, },
"confirmNewMasterPass": { "confirmNewMasterPass": {
"message": "Confirm new master password" "message": "Confirm new master password"
@ -2266,10 +2266,10 @@
"message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device." "message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device."
}, },
"resendNotification": { "resendNotification": {
"message": "Resend notification" "message": "Berbidali jakinarazpena"
}, },
"viewAllLoginOptions": { "viewAllLoginOptions": {
"message": "View all log in options" "message": "Ikusi erregistro guztiak ezarpenetan"
}, },
"notificationSentDevice": { "notificationSentDevice": {
"message": "A notification has been sent to your device." "message": "A notification has been sent to your device."
@ -2293,13 +2293,13 @@
"message": "Check known data breaches for this password" "message": "Check known data breaches for this password"
}, },
"important": { "important": {
"message": "Important:" "message": "Garrantzitsua:"
}, },
"masterPasswordHint": { "masterPasswordHint": {
"message": "Your master password cannot be recovered if you forget it!" "message": "Your master password cannot be recovered if you forget it!"
}, },
"characterMinimum": { "characterMinimum": {
"message": "$LENGTH$ character minimum", "message": "$LENGTH$ karaktere gutxienez",
"placeholders": { "placeholders": {
"length": { "length": {
"content": "$1", "content": "$1",
@ -2326,7 +2326,7 @@
"message": "Select an item from this screen, or explore other options in settings." "message": "Select an item from this screen, or explore other options in settings."
}, },
"gotIt": { "gotIt": {
"message": "Got it" "message": "Ulertuta"
}, },
"autofillSettings": { "autofillSettings": {
"message": "Auto-fill settings" "message": "Auto-fill settings"
@ -2359,25 +2359,25 @@
"message": "Logging in on" "message": "Logging in on"
}, },
"opensInANewWindow": { "opensInANewWindow": {
"message": "Opens in a new window" "message": "Leiho berri batean irekitzen da"
}, },
"deviceApprovalRequired": { "deviceApprovalRequired": {
"message": "Device approval required. Select an approval option below:" "message": "Device approval required. Select an approval option below:"
}, },
"rememberThisDevice": { "rememberThisDevice": {
"message": "Remember this device" "message": "Gogoratu gailu hau"
}, },
"uncheckIfPublicDevice": { "uncheckIfPublicDevice": {
"message": "Uncheck if using a public device" "message": "Uncheck if using a public device"
}, },
"approveFromYourOtherDevice": { "approveFromYourOtherDevice": {
"message": "Approve from your other device" "message": "Onartu zure beste gailutik"
}, },
"requestAdminApproval": { "requestAdminApproval": {
"message": "Request admin approval" "message": "Eskatu administratzailearen onarpena"
}, },
"approveWithMasterPassword": { "approveWithMasterPassword": {
"message": "Approve with master password" "message": "Onartu pasahitz nagusiarekin"
}, },
"ssoIdentifierRequired": { "ssoIdentifierRequired": {
"message": "Organization SSO identifier is required." "message": "Organization SSO identifier is required."
@ -2390,31 +2390,31 @@
"message": "Access denied. You do not have permission to view this page." "message": "Access denied. You do not have permission to view this page."
}, },
"general": { "general": {
"message": "General" "message": "Orokorra"
}, },
"display": { "display": {
"message": "Display" "message": "Bistaratzea"
}, },
"accountSuccessfullyCreated": { "accountSuccessfullyCreated": {
"message": "Account successfully created!" "message": "Kontua zuzen sortu da!"
}, },
"adminApprovalRequested": { "adminApprovalRequested": {
"message": "Admin approval requested" "message": "Administratzailearen onarpena eskatuta"
}, },
"adminApprovalRequestSentToAdmins": { "adminApprovalRequestSentToAdmins": {
"message": "Your request has been sent to your admin." "message": "Zure eskaera zure administratzaileari bidali zaio."
}, },
"youWillBeNotifiedOnceApproved": { "youWillBeNotifiedOnceApproved": {
"message": "You will be notified once approved." "message": "Jakinaraziko zaizu onartzen denean."
}, },
"troubleLoggingIn": { "troubleLoggingIn": {
"message": "Trouble logging in?" "message": "Arazoak saioa hasterakoan?"
}, },
"loginApproved": { "loginApproved": {
"message": "Login approved" "message": "Login approved"
}, },
"userEmailMissing": { "userEmailMissing": {
"message": "User email missing" "message": "Erabiltzailearen emaila falta da"
}, },
"deviceTrusted": { "deviceTrusted": {
"message": "Device trusted" "message": "Device trusted"
@ -2540,19 +2540,19 @@
"description": "Notification button text for starting a fileless import." "description": "Notification button text for starting a fileless import."
}, },
"importing": { "importing": {
"message": "Importing...", "message": "Inportatzen...",
"description": "Notification message for when an import is in progress." "description": "Notification message for when an import is in progress."
}, },
"dataSuccessfullyImported": { "dataSuccessfullyImported": {
"message": "Data successfully imported!", "message": "Datuak zuzen inportatu dira!",
"description": "Notification message for when an import has completed successfully." "description": "Notification message for when an import has completed successfully."
}, },
"dataImportFailed": { "dataImportFailed": {
"message": "Error importing. Check console for details.", "message": "Errorea gertatu da inportatzean. Begiratu xehetasunak kontsolan.",
"description": "Notification message for when an import has failed." "description": "Notification message for when an import has failed."
}, },
"importNetworkError": { "importNetworkError": {
"message": "Network error encountered during import.", "message": "Sareko errorea gertatu da inportatzerakoan.",
"description": "Notification message for when an import has failed due to a network error." "description": "Notification message for when an import has failed due to a network error."
}, },
"aliasDomain": { "aliasDomain": {
@ -2602,11 +2602,11 @@
"description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username"
}, },
"noItemsToShow": { "noItemsToShow": {
"message": "No items to show", "message": "Ez dago elementurik erakusteko",
"description": "Text to show in overlay if there are no matching items" "description": "Text to show in overlay if there are no matching items"
}, },
"newItem": { "newItem": {
"message": "New item", "message": "Elementu berria",
"description": "Button text to display in overlay when there are no matching items" "description": "Button text to display in overlay when there are no matching items"
}, },
"addNewVaultItem": { "addNewVaultItem": {
@ -2618,32 +2618,32 @@
"description": "Screen reader text for announcing when the overlay opens on the page" "description": "Screen reader text for announcing when the overlay opens on the page"
}, },
"turnOn": { "turnOn": {
"message": "Turn on" "message": "Piztu"
}, },
"ignore": { "ignore": {
"message": "Ignore" "message": "Ezikusi"
}, },
"importData": { "importData": {
"message": "Import data", "message": "Inportatu datuak",
"description": "Used for the header of the import dialog, the import button and within the file-password-prompt" "description": "Used for the header of the import dialog, the import button and within the file-password-prompt"
}, },
"importError": { "importError": {
"message": "Import error" "message": "Errorea inportatzerakoan"
}, },
"importErrorDesc": { "importErrorDesc": {
"message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." "message": "Inportatzen saiatu zaren datuekin arazo bat egon da. Mesedez, konpondu ondoren adierazten diren akatsak eta saiatu berriro."
}, },
"resolveTheErrorsBelowAndTryAgain": { "resolveTheErrorsBelowAndTryAgain": {
"message": "Resolve the errors below and try again." "message": "Konpondu beheko akatsak eta saiatu berriro."
}, },
"description": { "description": {
"message": "Description" "message": "Deskribapena"
}, },
"importSuccess": { "importSuccess": {
"message": "Data successfully imported" "message": "Datuak zuzen inportatu dira"
}, },
"importSuccessNumberOfItems": { "importSuccessNumberOfItems": {
"message": "A total of $AMOUNT$ items were imported.", "message": "Guztira $AMOUNT$ elementu inportatu dira.",
"placeholders": { "placeholders": {
"amount": { "amount": {
"content": "$1", "content": "$1",
@ -2652,7 +2652,7 @@
} }
}, },
"tryAgain": { "tryAgain": {
"message": "Try again" "message": "Saiatu berriro"
}, },
"verificationRequiredForActionSetPinToContinue": { "verificationRequiredForActionSetPinToContinue": {
"message": "Verification required for this action. Set a PIN to continue." "message": "Verification required for this action. Set a PIN to continue."
@ -2661,10 +2661,10 @@
"message": "Set PIN" "message": "Set PIN"
}, },
"verifyWithBiometrics": { "verifyWithBiometrics": {
"message": "Verify with biometrics" "message": "Egiaztatu biometria erabiliz"
}, },
"awaitingConfirmation": { "awaitingConfirmation": {
"message": "Awaiting confirmation" "message": "Baieztapenaren zain"
}, },
"couldNotCompleteBiometrics": { "couldNotCompleteBiometrics": {
"message": "Could not complete biometrics." "message": "Could not complete biometrics."
@ -2673,13 +2673,13 @@
"message": "Need a different method?" "message": "Need a different method?"
}, },
"useMasterPassword": { "useMasterPassword": {
"message": "Use master password" "message": "Erabili pasahitz nagusia"
}, },
"usePin": { "usePin": {
"message": "Use PIN" "message": "Erabili PIN kodea"
}, },
"useBiometrics": { "useBiometrics": {
"message": "Use biometrics" "message": "Erabili biometria"
}, },
"enterVerificationCodeSentToEmail": { "enterVerificationCodeSentToEmail": {
"message": "Enter the verification code that was sent to your email." "message": "Enter the verification code that was sent to your email."
@ -2688,10 +2688,10 @@
"message": "Resend code" "message": "Resend code"
}, },
"total": { "total": {
"message": "Total" "message": "Guztira"
}, },
"importWarning": { "importWarning": {
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", "message": "$ORGANIZATION$(e)ra datuak inportatzen ari zara. Zure datuak erakunde horretako kideekin parteka daitezke. Jarraitu nahi duzu?",
"placeholders": { "placeholders": {
"organization": { "organization": {
"content": "$1", "content": "$1",
@ -2810,7 +2810,7 @@
"message": "You do not have a matching login for this site." "message": "You do not have a matching login for this site."
}, },
"confirm": { "confirm": {
"message": "Confirm" "message": "Berretsi"
}, },
"savePasskey": { "savePasskey": {
"message": "Save passkey" "message": "Save passkey"

View File

@ -11,7 +11,7 @@
"description": "Extension description" "description": "Extension description"
}, },
"loginOrCreateNewAccount": { "loginOrCreateNewAccount": {
"message": "Käytä salattua holviasi kirjautumalla sisään tai tai luo uusi tili." "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili."
}, },
"createAccount": { "createAccount": {
"message": "Luo tili" "message": "Luo tili"
@ -802,7 +802,7 @@
"message": "Lue lisää" "message": "Lue lisää"
}, },
"authenticatorKeyTotp": { "authenticatorKeyTotp": {
"message": "Todennusaavain (TOTP)" "message": "Todennusavain (TOTP)"
}, },
"verificationCodeTotp": { "verificationCodeTotp": {
"message": "Todennuskoodi (TOTP)" "message": "Todennuskoodi (TOTP)"

View File

@ -17,18 +17,21 @@ import {
FactoryOptions, FactoryOptions,
factory, factory,
} from "../../../platform/background/service-factories/factory-options"; } from "../../../platform/background/service-factories/factory-options";
import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory";
import { import {
stateServiceFactory, internalMasterPasswordServiceFactory,
StateServiceInitOptions, MasterPasswordServiceInitOptions,
} from "../../../platform/background/service-factories/state-service.factory"; } from "./master-password-service.factory";
type AuthRequestServiceFactoryOptions = FactoryOptions; type AuthRequestServiceFactoryOptions = FactoryOptions;
export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions & export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions &
AppIdServiceInitOptions & AppIdServiceInitOptions &
AccountServiceInitOptions &
MasterPasswordServiceInitOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
ApiServiceInitOptions & ApiServiceInitOptions;
StateServiceInitOptions;
export function authRequestServiceFactory( export function authRequestServiceFactory(
cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices, cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices,
@ -41,9 +44,10 @@ export function authRequestServiceFactory(
async () => async () =>
new AuthRequestService( new AuthRequestService(
await appIdServiceFactory(cache, opts), await appIdServiceFactory(cache, opts),
await accountServiceFactory(cache, opts),
await internalMasterPasswordServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await apiServiceFactory(cache, opts), await apiServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
), ),
); );
} }

View File

@ -31,6 +31,11 @@ import {
StateProviderInitOptions, StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory"; } from "../../../platform/background/service-factories/state-provider.factory";
import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory";
import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "./master-password-service.factory";
import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory"; import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory";
type KeyConnectorServiceFactoryOptions = FactoryOptions & { type KeyConnectorServiceFactoryOptions = FactoryOptions & {
@ -40,6 +45,8 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & {
}; };
export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions &
AccountServiceInitOptions &
MasterPasswordServiceInitOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
ApiServiceInitOptions & ApiServiceInitOptions &
TokenServiceInitOptions & TokenServiceInitOptions &
@ -58,6 +65,8 @@ export function keyConnectorServiceFactory(
opts, opts,
async () => async () =>
new KeyConnectorService( new KeyConnectorService(
await accountServiceFactory(cache, opts),
await internalMasterPasswordServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await apiServiceFactory(cache, opts), await apiServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts), await tokenServiceFactory(cache, opts),

View File

@ -59,6 +59,7 @@ import {
PasswordStrengthServiceInitOptions, PasswordStrengthServiceInitOptions,
} from "../../../tools/background/service_factories/password-strength-service.factory"; } from "../../../tools/background/service_factories/password-strength-service.factory";
import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory";
import { import {
authRequestServiceFactory, authRequestServiceFactory,
AuthRequestServiceInitOptions, AuthRequestServiceInitOptions,
@ -71,6 +72,10 @@ import {
keyConnectorServiceFactory, keyConnectorServiceFactory,
KeyConnectorServiceInitOptions, KeyConnectorServiceInitOptions,
} from "./key-connector-service.factory"; } from "./key-connector-service.factory";
import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "./master-password-service.factory";
import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory"; import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory";
import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory"; import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory";
import { import {
@ -81,6 +86,8 @@ import {
type LoginStrategyServiceFactoryOptions = FactoryOptions; type LoginStrategyServiceFactoryOptions = FactoryOptions;
export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions & export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions &
AccountServiceInitOptions &
MasterPasswordServiceInitOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
ApiServiceInitOptions & ApiServiceInitOptions &
TokenServiceInitOptions & TokenServiceInitOptions &
@ -111,6 +118,8 @@ export function loginStrategyServiceFactory(
opts, opts,
async () => async () =>
new LoginStrategyService( new LoginStrategyService(
await accountServiceFactory(cache, opts),
await internalMasterPasswordServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await apiServiceFactory(cache, opts), await apiServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts), await tokenServiceFactory(cache, opts),

View File

@ -0,0 +1,42 @@
import {
InternalMasterPasswordServiceAbstraction,
MasterPasswordServiceAbstraction,
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
type MasterPasswordServiceFactoryOptions = FactoryOptions;
export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions &
StateProviderInitOptions;
export function internalMasterPasswordServiceFactory(
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
opts: MasterPasswordServiceInitOptions,
): Promise<InternalMasterPasswordServiceAbstraction> {
return factory(
cache,
"masterPasswordService",
opts,
async () => new MasterPasswordService(await stateProviderFactory(cache, opts)),
);
}
export async function masterPasswordServiceFactory(
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
opts: MasterPasswordServiceInitOptions,
): Promise<MasterPasswordServiceAbstraction> {
return (await internalMasterPasswordServiceFactory(
cache,
opts,
)) as MasterPasswordServiceAbstraction;
}

View File

@ -31,6 +31,11 @@ import {
stateServiceFactory, stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory"; } from "../../../platform/background/service-factories/state-service.factory";
import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory";
import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "./master-password-service.factory";
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory";
import { import {
userDecryptionOptionsServiceFactory, userDecryptionOptionsServiceFactory,
@ -46,6 +51,8 @@ type UserVerificationServiceFactoryOptions = FactoryOptions;
export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryOptions & export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryOptions &
StateServiceInitOptions & StateServiceInitOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
AccountServiceInitOptions &
MasterPasswordServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
UserVerificationApiServiceInitOptions & UserVerificationApiServiceInitOptions &
UserDecryptionOptionsServiceInitOptions & UserDecryptionOptionsServiceInitOptions &
@ -66,6 +73,8 @@ export function userVerificationServiceFactory(
new UserVerificationService( new UserVerificationService(
await stateServiceFactory(cache, opts), await stateServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await accountServiceFactory(cache, opts),
await internalMasterPasswordServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await userVerificationApiServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts),

View File

@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -41,6 +42,7 @@ export class LockComponent extends BaseLockComponent {
fido2PopoutSessionData$ = fido2PopoutSessionData$(); fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor( constructor(
masterPasswordService: InternalMasterPasswordServiceAbstraction,
router: Router, router: Router,
i18nService: I18nService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
@ -66,6 +68,7 @@ export class LockComponent extends BaseLockComponent {
accountService: AccountService, accountService: AccountService,
) { ) {
super( super(
masterPasswordService,
router, router,
i18nService, i18nService,
platformUtilsService, platformUtilsService,

View File

@ -1,65 +1,9 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
@Component({ @Component({
selector: "app-set-password", selector: "app-set-password",
templateUrl: "set-password.component.html", templateUrl: "set-password.component.html",
}) })
export class SetPasswordComponent extends BaseSetPasswordComponent { export class SetPasswordComponent extends BaseSetPasswordComponent {}
constructor(
apiService: ApiService,
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
stateService: StateService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
platformUtilsService: PlatformUtilsService,
policyApiService: PolicyApiServiceAbstraction,
policyService: PolicyService,
router: Router,
syncService: SyncService,
route: ActivatedRoute,
organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyApiService,
policyService,
router,
apiService,
syncService,
route,
stateService,
organizationApiService,
organizationUserService,
userDecryptionOptionsService,
ssoLoginService,
dialogService,
);
}
}

View File

@ -9,7 +9,9 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@ -45,7 +47,9 @@ export class SsoComponent extends BaseSsoComponent {
logService: LogService, logService: LogService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigService, configService: ConfigService,
protected authService: AuthService, masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
private authService: AuthService,
@Inject(WINDOW) private win: Window, @Inject(WINDOW) private win: Window,
) { ) {
super( super(
@ -63,6 +67,8 @@ export class SsoComponent extends BaseSsoComponent {
logService, logService,
userDecryptionOptionsService, userDecryptionOptionsService,
configService, configService,
masterPasswordService,
accountService,
); );
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {

View File

@ -11,6 +11,8 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@ -58,6 +60,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
configService: ConfigService, configService: ConfigService,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
private dialogService: DialogService, private dialogService: DialogService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
@Inject(WINDOW) protected win: Window, @Inject(WINDOW) protected win: Window,
private browserMessagingApi: ZonedMessageListenerService, private browserMessagingApi: ZonedMessageListenerService,
) { ) {
@ -78,6 +82,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
userDecryptionOptionsService, userDecryptionOptionsService,
ssoLoginService, ssoLoginService,
configService, configService,
masterPasswordService,
accountService,
); );
super.onSuccessfulLogin = async () => { super.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@ -8,7 +8,6 @@ import {
generateRandomCustomElementName, generateRandomCustomElementName,
sendExtensionMessage, sendExtensionMessage,
setElementStyles, setElementStyles,
getFromLocalStorage,
setupExtensionDisconnectAction, setupExtensionDisconnectAction,
setupAutofillInitDisconnectAction, setupAutofillInitDisconnectAction,
} from "./index"; } from "./index";
@ -124,33 +123,6 @@ describe("setElementStyles", () => {
}); });
}); });
describe("getFromLocalStorage", () => {
it("returns a promise with the storage object pulled from the extension storage api", async () => {
const localStorage: Record<string, any> = {
testValue: "test",
another: "another",
};
jest.spyOn(chrome.storage.local, "get").mockImplementation((keys, callback) => {
const localStorageObject: Record<string, string> = {};
if (typeof keys === "string") {
localStorageObject[keys] = localStorage[keys];
} else if (Array.isArray(keys)) {
for (const key of keys) {
localStorageObject[key] = localStorage[key];
}
}
callback(localStorageObject);
});
const returnValue = await getFromLocalStorage("testValue");
expect(chrome.storage.local.get).toHaveBeenCalled();
expect(returnValue).toEqual({ testValue: "test" });
});
});
describe("setupExtensionDisconnectAction", () => { describe("setupExtensionDisconnectAction", () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();

View File

@ -106,18 +106,6 @@ function setElementStyles(
} }
} }
/**
* Get data from local storage based on the keys provided.
*
* @param keys - String or array of strings of keys to get from local storage
* @deprecated Do not call this, use state-relevant services instead
*/
async function getFromLocalStorage(keys: string | string[]): Promise<Record<string, any>> {
return new Promise((resolve) => {
chrome.storage.local.get(keys, (storage: Record<string, any>) => resolve(storage));
});
}
/** /**
* Sets up a long-lived connection with the extension background * Sets up a long-lived connection with the extension background
* and triggers an onDisconnect event if the extension context * and triggers an onDisconnect event if the extension context
@ -279,7 +267,6 @@ export {
buildSvgDomElement, buildSvgDomElement,
sendExtensionMessage, sendExtensionMessage,
setElementStyles, setElementStyles,
getFromLocalStorage,
setupExtensionDisconnectAction, setupExtensionDisconnectAction,
setupAutofillInitDisconnectAction, setupAutofillInitDisconnectAction,
elementIsFillableFormField, elementIsFillableFormField,

View File

@ -10,6 +10,7 @@ import {
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
AuthRequestService, AuthRequestService,
LoginEmailServiceAbstraction, LoginEmailServiceAbstraction,
LoginEmailService,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@ -32,6 +33,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
@ -46,6 +48,7 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
@ -232,7 +235,7 @@ import RuntimeBackground from "./runtime.background";
export default class MainBackground { export default class MainBackground {
messagingService: MessagingServiceAbstraction; messagingService: MessagingServiceAbstraction;
storageService: AbstractStorageService & ObservableStorageService; storageService: BrowserLocalStorageService;
secureStorageService: AbstractStorageService; secureStorageService: AbstractStorageService;
memoryStorageService: AbstractMemoryStorageService; memoryStorageService: AbstractMemoryStorageService;
memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService;
@ -242,6 +245,7 @@ export default class MainBackground {
keyGenerationService: KeyGenerationServiceAbstraction; keyGenerationService: KeyGenerationServiceAbstraction;
cryptoService: CryptoServiceAbstraction; cryptoService: CryptoServiceAbstraction;
cryptoFunctionService: CryptoFunctionServiceAbstraction; cryptoFunctionService: CryptoFunctionServiceAbstraction;
masterPasswordService: InternalMasterPasswordServiceAbstraction;
tokenService: TokenServiceAbstraction; tokenService: TokenServiceAbstraction;
appIdService: AppIdServiceAbstraction; appIdService: AppIdServiceAbstraction;
apiService: ApiServiceAbstraction; apiService: ApiServiceAbstraction;
@ -361,7 +365,8 @@ export default class MainBackground {
const logoutCallback = async (expired: boolean, userId?: UserId) => const logoutCallback = async (expired: boolean, userId?: UserId) =>
await this.logout(expired, userId); await this.logout(expired, userId);
this.messagingService = this.popupOnlyContext this.messagingService =
this.isPrivateMode && BrowserApi.isManifestVersion(2)
? new BrowserMessagingPrivateModeBackgroundService() ? new BrowserMessagingPrivateModeBackgroundService()
: new BrowserMessagingService(); : new BrowserMessagingService();
this.logService = new ConsoleLogService(false); this.logService = new ConsoleLogService(false);
@ -405,7 +410,8 @@ export default class MainBackground {
storageServiceProvider, storageServiceProvider,
); );
this.encryptService = flagEnabled("multithreadDecryption") this.encryptService =
flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2)
? new MultithreadEncryptServiceImplementation( ? new MultithreadEncryptServiceImplementation(
this.cryptoFunctionService, this.cryptoFunctionService,
this.logService, this.logService,
@ -480,8 +486,11 @@ export default class MainBackground {
const themeStateService = new DefaultThemeStateService(this.globalStateProvider); const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
this.cryptoService = new BrowserCryptoService( this.cryptoService = new BrowserCryptoService(
this.masterPasswordService,
this.keyGenerationService, this.keyGenerationService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.encryptService, this.encryptService,
@ -508,7 +517,7 @@ export default class MainBackground {
this.apiService, this.apiService,
this.fileUploadService, this.fileUploadService,
); );
this.searchService = new SearchService(this.logService, this.i18nService); this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider);
this.collectionService = new CollectionService( this.collectionService = new CollectionService(
this.cryptoService, this.cryptoService,
@ -525,6 +534,8 @@ export default class MainBackground {
this.badgeSettingsService = new BadgeSettingsService(this.stateProvider); this.badgeSettingsService = new BadgeSettingsService(this.stateProvider);
this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
this.keyConnectorService = new KeyConnectorService( this.keyConnectorService = new KeyConnectorService(
this.accountService,
this.masterPasswordService,
this.cryptoService, this.cryptoService,
this.apiService, this.apiService,
this.tokenService, this.tokenService,
@ -550,10 +561,13 @@ export default class MainBackground {
const backgroundMessagingService = new (class extends MessagingServiceAbstraction { const backgroundMessagingService = new (class extends MessagingServiceAbstraction {
// AuthService should send the messages to the background not popup. // AuthService should send the messages to the background not popup.
send = (subscriber: string, arg: any = {}) => { send = (subscriber: string, arg: any = {}) => {
if (BrowserApi.isManifestVersion(3)) {
that.messagingService.send(subscriber, arg);
return;
}
const message = Object.assign({}, { command: subscriber }, arg); const message = Object.assign({}, { command: subscriber }, arg);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void that.runtimeBackground.processMessage(message, that as any);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
that.runtimeBackground.processMessage(message, that as any);
}; };
})(); })();
@ -578,9 +592,10 @@ export default class MainBackground {
this.authRequestService = new AuthRequestService( this.authRequestService = new AuthRequestService(
this.appIdService, this.appIdService,
this.accountService,
this.masterPasswordService,
this.cryptoService, this.cryptoService,
this.apiService, this.apiService,
this.stateService,
); );
this.authService = new AuthService( this.authService = new AuthService(
@ -596,7 +611,11 @@ export default class MainBackground {
this.stateProvider, this.stateProvider,
); );
this.loginEmailService = new LoginEmailService(this.stateProvider);
this.loginStrategyService = new LoginStrategyService( this.loginStrategyService = new LoginStrategyService(
this.accountService,
this.masterPasswordService,
this.cryptoService, this.cryptoService,
this.apiService, this.apiService,
this.tokenService, this.tokenService,
@ -672,6 +691,8 @@ export default class MainBackground {
this.userVerificationService = new UserVerificationService( this.userVerificationService = new UserVerificationService(
this.stateService, this.stateService,
this.cryptoService, this.cryptoService,
this.accountService,
this.masterPasswordService,
this.i18nService, this.i18nService,
this.userVerificationApiService, this.userVerificationApiService,
this.userDecryptionOptionsService, this.userDecryptionOptionsService,
@ -694,6 +715,8 @@ export default class MainBackground {
this.vaultSettingsService = new VaultSettingsService(this.stateProvider); this.vaultSettingsService = new VaultSettingsService(this.stateProvider);
this.vaultTimeoutService = new VaultTimeoutService( this.vaultTimeoutService = new VaultTimeoutService(
this.accountService,
this.masterPasswordService,
this.cipherService, this.cipherService,
this.folderService, this.folderService,
this.collectionService, this.collectionService,
@ -729,6 +752,8 @@ export default class MainBackground {
this.providerService = new ProviderService(this.stateProvider); this.providerService = new ProviderService(this.stateProvider);
this.syncService = new SyncService( this.syncService = new SyncService(
this.masterPasswordService,
this.accountService,
this.apiService, this.apiService,
this.domainSettingsService, this.domainSettingsService,
this.folderService, this.folderService,
@ -878,6 +903,8 @@ export default class MainBackground {
this.fido2Service, this.fido2Service,
); );
this.nativeMessagingBackground = new NativeMessagingBackground( this.nativeMessagingBackground = new NativeMessagingBackground(
this.accountService,
this.masterPasswordService,
this.cryptoService, this.cryptoService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.runtimeBackground, this.runtimeBackground,
@ -1108,7 +1135,7 @@ export default class MainBackground {
const status = await this.authService.getAuthStatus(userId); const status = await this.authService.getAuthStatus(userId);
const forcePasswordReset = const forcePasswordReset =
(await this.stateService.getForceSetPasswordReason({ userId: userId })) != (await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId))) !=
ForceSetPasswordReason.None; ForceSetPasswordReason.None;
await this.systemService.clearPendingClipboard(); await this.systemService.clearPendingClipboard();
@ -1159,10 +1186,10 @@ export default class MainBackground {
const newActiveUser = await this.stateService.clean({ userId: userId }); const newActiveUser = await this.stateService.clean({ userId: userId });
if (userId == null || userId === currentUserId) { if (userId == null || userId === currentUserId) {
this.searchService.clearIndex(); await this.searchService.clearIndex();
} }
await this.stateEventRunnerService.handleEvent("logout", currentUserId as UserId); await this.stateEventRunnerService.handleEvent("logout", userId);
if (newActiveUser != null) { if (newActiveUser != null) {
// we have a new active user, do not continue tearing down application // we have a new active user, do not continue tearing down application
@ -1237,18 +1264,8 @@ export default class MainBackground {
return; return;
} }
const getStorage = (): Promise<any> => const storage = await this.storageService.getAll();
new Promise((resolve) => { await this.storageService.clear();
chrome.storage.local.get(null, (o: any) => resolve(o));
});
const clearStorage = (): Promise<void> =>
new Promise((resolve) => {
chrome.storage.local.clear(() => resolve());
});
const storage = await getStorage();
await clearStorage();
for (const key in storage) { for (const key in storage) {
// eslint-disable-next-line // eslint-disable-next-line

View File

@ -1,6 +1,8 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -71,6 +73,8 @@ export class NativeMessagingBackground {
private validatingFingerprint: boolean; private validatingFingerprint: boolean;
constructor( constructor(
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,
private runtimeBackground: RuntimeBackground, private runtimeBackground: RuntimeBackground,
@ -336,10 +340,14 @@ export class NativeMessagingBackground {
) as UserKey; ) as UserKey;
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey);
} else if (message.keyB64) { } else if (message.keyB64) {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
// Backwards compatibility to support cases in which the user hasn't updated their desktop app // Backwards compatibility to support cases in which the user hasn't updated their desktop app
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472)
let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey();
encUserKey ||= await this.stateService.getMasterKeyEncryptedUserKey(); const encUserKey =
encUserKeyPrim != null
? new EncString(encUserKeyPrim)
: await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId);
if (!encUserKey) { if (!encUserKey) {
throw new Error("No encrypted user key found"); throw new Error("No encrypted user key found");
} }
@ -348,9 +356,9 @@ export class NativeMessagingBackground {
) as MasterKey; ) as MasterKey;
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey( const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(
masterKey, masterKey,
new EncString(encUserKey), encUserKey,
); );
await this.cryptoService.setMasterKey(masterKey); await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey);
} else { } else {
throw new Error("No key received"); throw new Error("No key received");

View File

@ -14,12 +14,17 @@ import {
logServiceFactory, logServiceFactory,
LogServiceInitOptions, LogServiceInitOptions,
} from "../../platform/background/service-factories/log-service.factory"; } from "../../platform/background/service-factories/log-service.factory";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../platform/background/service-factories/state-provider.factory";
type SearchServiceFactoryOptions = FactoryOptions; type SearchServiceFactoryOptions = FactoryOptions;
export type SearchServiceInitOptions = SearchServiceFactoryOptions & export type SearchServiceInitOptions = SearchServiceFactoryOptions &
LogServiceInitOptions & LogServiceInitOptions &
I18nServiceInitOptions; I18nServiceInitOptions &
StateProviderInitOptions;
export function searchServiceFactory( export function searchServiceFactory(
cache: { searchService?: AbstractSearchService } & CachedServices, cache: { searchService?: AbstractSearchService } & CachedServices,
@ -33,6 +38,7 @@ export function searchServiceFactory(
new SearchService( new SearchService(
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
), ),
); );
} }

View File

@ -1,9 +1,17 @@
import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService as AbstractVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import {
accountServiceFactory,
AccountServiceInitOptions,
} from "../../auth/background/service-factories/account-service.factory";
import { import {
authServiceFactory, authServiceFactory,
AuthServiceInitOptions, AuthServiceInitOptions,
} from "../../auth/background/service-factories/auth-service.factory"; } from "../../auth/background/service-factories/auth-service.factory";
import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "../../auth/background/service-factories/master-password-service.factory";
import { import {
CryptoServiceInitOptions, CryptoServiceInitOptions,
cryptoServiceFactory, cryptoServiceFactory,
@ -57,6 +65,8 @@ type VaultTimeoutServiceFactoryOptions = FactoryOptions & {
}; };
export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions &
AccountServiceInitOptions &
MasterPasswordServiceInitOptions &
CipherServiceInitOptions & CipherServiceInitOptions &
FolderServiceInitOptions & FolderServiceInitOptions &
CollectionServiceInitOptions & CollectionServiceInitOptions &
@ -79,6 +89,8 @@ export function vaultTimeoutServiceFactory(
opts, opts,
async () => async () =>
new VaultTimeoutService( new VaultTimeoutService(
await accountServiceFactory(cache, opts),
await internalMasterPasswordServiceFactory(cache, opts),
await cipherServiceFactory(cache, opts), await cipherServiceFactory(cache, opts),
await folderServiceFactory(cache, opts), await folderServiceFactory(cache, opts),
await collectionServiceFactory(cache, opts), await collectionServiceFactory(cache, opts),

View File

@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.3.1", "version": "2024.4.1",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0", "minimum_chrome_version": "102.0",
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.3.1", "version": "2024.4.1",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@ -4,6 +4,10 @@ import {
AccountServiceInitOptions, AccountServiceInitOptions,
accountServiceFactory, accountServiceFactory,
} from "../../../auth/background/service-factories/account-service.factory"; } from "../../../auth/background/service-factories/account-service.factory";
import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "../../../auth/background/service-factories/master-password-service.factory";
import { import {
StateServiceInitOptions, StateServiceInitOptions,
stateServiceFactory, stateServiceFactory,
@ -34,6 +38,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider
type CryptoServiceFactoryOptions = FactoryOptions; type CryptoServiceFactoryOptions = FactoryOptions;
export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
MasterPasswordServiceInitOptions &
KeyGenerationServiceInitOptions & KeyGenerationServiceInitOptions &
CryptoFunctionServiceInitOptions & CryptoFunctionServiceInitOptions &
EncryptServiceInitOptions & EncryptServiceInitOptions &
@ -53,6 +58,7 @@ export function cryptoServiceFactory(
opts, opts,
async () => async () =>
new BrowserCryptoService( new BrowserCryptoService(
await internalMasterPasswordServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts),
await cryptoFunctionServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts), await encryptServiceFactory(cache, opts),

View File

@ -93,6 +93,10 @@ export class SessionSyncer {
} }
async update(serializedValue: any) { async update(serializedValue: any) {
if (!serializedValue) {
return;
}
const unBuiltValue = JSON.parse(serializedValue); const unBuiltValue = JSON.parse(serializedValue);
if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) { if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) {
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
@ -104,6 +108,10 @@ export class SessionSyncer {
} }
private async updateSession(value: any) { private async updateSession(value: any) {
if (!value) {
return;
}
const serializedValue = JSON.stringify(value); const serializedValue = JSON.stringify(value);
if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) { if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) {
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);

View File

@ -10,6 +10,8 @@ import { fromChromeEvent } from "../../browser/from-chrome-event";
export const serializationIndicator = "__json__"; export const serializationIndicator = "__json__";
type serializedObject = { [serializationIndicator]: true; value: string };
export const objToStore = (obj: any) => { export const objToStore = (obj: any) => {
if (obj == null) { if (obj == null) {
return null; return null;
@ -61,11 +63,7 @@ export default abstract class AbstractChromeStorageService
return new Promise((resolve) => { return new Promise((resolve) => {
this.chromeStorageApi.get(key, (obj: any) => { this.chromeStorageApi.get(key, (obj: any) => {
if (obj != null && obj[key] != null) { if (obj != null && obj[key] != null) {
let value = obj[key]; resolve(this.processGetObject(obj[key]));
if (value[serializationIndicator] && typeof value.value === "string") {
value = JSON.parse(value.value);
}
resolve(value as T);
return; return;
} }
resolve(null); resolve(null);
@ -95,4 +93,22 @@ export default abstract class AbstractChromeStorageService
}); });
}); });
} }
/** Backwards compatible resolution of retrieved object with new serialized storage */
protected processGetObject<T>(obj: T | serializedObject): T | null {
if (this.isSerialized(obj)) {
obj = JSON.parse(obj.value);
}
return obj as T;
}
/** Type guard for whether an object is tagged as serialized */
protected isSerialized<T>(value: T | serializedObject): value is serializedObject {
const asSerialized = value as serializedObject;
return (
asSerialized != null &&
asSerialized[serializationIndicator] &&
typeof asSerialized.value === "string"
);
}
} }

View File

@ -66,6 +66,7 @@ describe("ChromeStorageApiService", () => {
describe("get", () => { describe("get", () => {
let getMock: jest.Mock; let getMock: jest.Mock;
const key = "key";
beforeEach(() => { beforeEach(() => {
// setup get // setup get
@ -76,7 +77,6 @@ describe("ChromeStorageApiService", () => {
}); });
it("returns a stored value when it is serialized", async () => { it("returns a stored value when it is serialized", async () => {
const key = "key";
const value = { key: "value" }; const value = { key: "value" };
store[key] = objToStore(value); store[key] = objToStore(value);
const result = await service.get(key); const result = await service.get(key);
@ -84,7 +84,6 @@ describe("ChromeStorageApiService", () => {
}); });
it("returns a stored value when it is not serialized", async () => { it("returns a stored value when it is not serialized", async () => {
const key = "key";
const value = "value"; const value = "value";
store[key] = value; store[key] = value;
const result = await service.get(key); const result = await service.get(key);
@ -95,5 +94,12 @@ describe("ChromeStorageApiService", () => {
const result = await service.get("key"); const result = await service.get("key");
expect(result).toBeNull(); expect(result).toBeNull();
}); });
it("returns null when the stored object is null", async () => {
store[key] = null;
const result = await service.get(key);
expect(result).toBeNull();
});
}); });
}); });

View File

@ -1,6 +1,7 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -17,6 +18,7 @@ import { UserKey } from "@bitwarden/common/types/key";
export class BrowserCryptoService extends CryptoService { export class BrowserCryptoService extends CryptoService {
constructor( constructor(
masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService, keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
encryptService: EncryptService, encryptService: EncryptService,
@ -28,6 +30,7 @@ export class BrowserCryptoService extends CryptoService {
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
) { ) {
super( super(
masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
encryptService, encryptService,

View File

@ -0,0 +1,89 @@
import { objToStore } from "./abstractions/abstract-chrome-storage-api.service";
import BrowserLocalStorageService from "./browser-local-storage.service";
describe("BrowserLocalStorageService", () => {
let service: BrowserLocalStorageService;
let store: Record<any, any>;
beforeEach(() => {
store = {};
service = new BrowserLocalStorageService();
});
describe("clear", () => {
let clearMock: jest.Mock;
beforeEach(() => {
clearMock = chrome.storage.local.clear as jest.Mock;
});
it("uses the api to clear", async () => {
await service.clear();
expect(clearMock).toHaveBeenCalledTimes(1);
});
});
describe("getAll", () => {
let getMock: jest.Mock;
beforeEach(() => {
// setup get
getMock = chrome.storage.local.get as jest.Mock;
getMock.mockImplementation((key, callback) => {
if (key == null) {
callback(store);
} else {
callback({ [key]: store[key] });
}
});
});
it("returns all values", async () => {
store["key1"] = "string";
store["key2"] = 0;
const result = await service.getAll();
expect(result).toEqual(store);
});
it("handles empty stores", async () => {
const result = await service.getAll();
expect(result).toEqual({});
});
it("handles stores with null values", async () => {
store["key"] = null;
const result = await service.getAll();
expect(result).toEqual(store);
});
it("handles values processed for storage", async () => {
const obj = { test: 2 };
const key = "key";
store[key] = objToStore(obj);
const result = await service.getAll();
expect(result).toEqual({
[key]: obj,
});
});
// This is a test of backwards compatibility before local storage was serialized.
it("handles values that were stored without processing for storage", async () => {
const obj = { test: 2 };
const key = "key";
store[key] = obj;
const result = await service.getAll();
expect(result).toEqual({
[key]: obj,
});
});
});
});

View File

@ -4,4 +4,32 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer
constructor() { constructor() {
super(chrome.storage.local); super(chrome.storage.local);
} }
/**
* Clears local storage
*/
async clear() {
await chrome.storage.local.clear();
}
/**
* Retrieves all objects stored in local storage.
*
* @remarks This method processes values prior to resolving, do not use `chrome.storage.local` directly
* @returns Promise resolving to keyed object of all stored data
*/
async getAll(): Promise<Record<string, unknown>> {
return new Promise((resolve) => {
this.chromeStorageApi.get(null, (allStorage) => {
const resolved = Object.entries(allStorage).reduce(
(agg, [key, value]) => {
agg[key] = this.processGetObject(value);
return agg;
},
{} as Record<string, unknown>,
);
resolve(resolved);
});
});
}
} }

View File

@ -1,17 +1,14 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SearchService } from "@bitwarden/common/services/search.service"; import { SearchService } from "@bitwarden/common/services/search.service";
export class PopupSearchService extends SearchService { export class PopupSearchService extends SearchService {
constructor( constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) {
private mainSearchService: SearchService, super(logService, i18nService, stateProvider);
logService: LogService,
i18nService: I18nService,
) {
super(logService, i18nService);
} }
clearIndex() { clearIndex(): Promise<void> {
throw new Error("Not available."); throw new Error("Not available.");
} }
@ -19,7 +16,7 @@ export class PopupSearchService extends SearchService {
throw new Error("Not available."); throw new Error("Not available.");
} }
getIndexForSearch() { async getIndexForSearch() {
return this.mainSearchService.getIndexForSearch(); return await super.getIndexForSearch();
} }
} }

View File

@ -62,6 +62,7 @@ import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/p
import { import {
AbstractMemoryStorageService, AbstractMemoryStorageService,
AbstractStorageService, AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service"; } from "@bitwarden/common/platform/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
@ -74,17 +75,15 @@ import {
GlobalStateProvider, GlobalStateProvider,
StateProvider, StateProvider,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
import { SearchService } from "@bitwarden/common/services/search.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { UnauthGuardService } from "../../auth/popup/services"; import { UnauthGuardService } from "../../auth/popup/services";
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
@ -159,7 +158,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: MessagingService, provide: MessagingService,
useFactory: () => { useFactory: () => {
return needsBackgroundInit return needsBackgroundInit && BrowserApi.isManifestVersion(2)
? new BrowserMessagingPrivateModePopupService() ? new BrowserMessagingPrivateModePopupService()
: new BrowserMessagingService(); : new BrowserMessagingService();
}, },
@ -187,19 +186,8 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: SearchServiceAbstraction, provide: SearchServiceAbstraction,
useFactory: (logService: LogService, i18nService: I18nServiceAbstraction) => { useClass: PopupSearchService,
return new PopupSearchService( deps: [LogService, I18nServiceAbstraction, StateProvider],
getBgService<SearchService>("searchService")(),
logService,
i18nService,
);
},
deps: [LogService, I18nServiceAbstraction],
}),
safeProvider({
provide: CipherFileUploadService,
useFactory: getBgService<CipherFileUploadService>("cipherFileUploadService"),
deps: [],
}), }),
safeProvider({ safeProvider({
provide: CipherService, provide: CipherService,
@ -231,11 +219,6 @@ const safeProviders: SafeProvider[] = [
useClass: BrowserEnvironmentService, useClass: BrowserEnvironmentService,
deps: [LogService, StateProvider, AccountServiceAbstraction], deps: [LogService, StateProvider, AccountServiceAbstraction],
}), }),
safeProvider({
provide: TotpService,
useFactory: getBgService<TotpService>("totpService"),
deps: [],
}),
safeProvider({ safeProvider({
provide: I18nServiceAbstraction, provide: I18nServiceAbstraction,
useFactory: (globalStateProvider: GlobalStateProvider) => { useFactory: (globalStateProvider: GlobalStateProvider) => {
@ -252,6 +235,11 @@ const safeProviders: SafeProvider[] = [
}, },
deps: [EncryptService], deps: [EncryptService],
}), }),
safeProvider({
provide: TotpServiceAbstraction,
useClass: TotpService,
deps: [CryptoFunctionService, LogService],
}),
safeProvider({ safeProvider({
provide: AuthRequestServiceAbstraction, provide: AuthRequestServiceAbstraction,
useFactory: getBgService<AuthRequestServiceAbstraction>("authRequestService"), useFactory: getBgService<AuthRequestServiceAbstraction>("authRequestService"),
@ -325,7 +313,7 @@ const safeProviders: SafeProvider[] = [
deps: [ deps: [
CipherService, CipherService,
AutofillSettingsServiceAbstraction, AutofillSettingsServiceAbstraction,
TotpService, TotpServiceAbstraction,
EventCollectionServiceAbstraction, EventCollectionServiceAbstraction,
LogService, LogService,
DomainSettingsService, DomainSettingsService,
@ -333,11 +321,6 @@ const safeProviders: SafeProvider[] = [
BillingAccountProfileStateService, BillingAccountProfileStateService,
], ],
}), }),
safeProvider({
provide: VaultExportServiceAbstraction,
useFactory: getBgService<VaultExportServiceAbstraction>("exportService"),
deps: [],
}),
safeProvider({ safeProvider({
provide: KeyConnectorService, provide: KeyConnectorService,
useFactory: getBgService<KeyConnectorService>("keyConnectorService"), useFactory: getBgService<KeyConnectorService>("keyConnectorService"),
@ -387,7 +370,15 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: OBSERVABLE_MEMORY_STORAGE, provide: OBSERVABLE_MEMORY_STORAGE,
useClass: ForegroundMemoryStorageService, useFactory: () => {
if (BrowserApi.isManifestVersion(2)) {
return new ForegroundMemoryStorageService();
}
return getBgService<AbstractStorageService & ObservableStorageService>(
"memoryStorageForStateProviders",
)();
},
deps: [], deps: [],
}), }),
safeProvider({ safeProvider({

View File

@ -171,9 +171,7 @@ export class SendGroupingsComponent extends BaseSendComponent {
} }
showSearching() { showSearching() {
return ( return this.hasSearched || (!this.searchPending && this.isSearchable);
this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText))
);
} }
private calculateTypeCounts() { private calculateTypeCounts() {

View File

@ -6,7 +6,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -31,7 +31,7 @@ export class ActionButtonsComponent implements OnInit, OnDestroy {
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private totpService: TotpService, private totpService: TotpServiceAbstraction,
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
) {} ) {}

View File

@ -311,7 +311,7 @@ export class Fido2Component implements OnInit, OnDestroy {
} }
protected async search() { protected async search() {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = await this.searchService.isSearchable(this.searchText);
this.searchPending = true; this.searchPending = true;
if (this.hasSearched) { if (this.hasSearched) {
this.displayedCiphers = await this.searchService.searchCiphers( this.displayedCiphers = await this.searchService.searchCiphers(

View File

@ -36,19 +36,32 @@
</div> </div>
<ng-container *ngIf="loaded"> <ng-container *ngIf="loaded">
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select> <app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
<app-callout *ngIf="showHowToAutofill" type="info" title="{{ 'howToAutofill' | i18n }}"> <app-callout
<p>{{ autofillCalloutText }}</p> *ngIf="
(unassignedItemsBannerEnabled$ | async) &&
(unassignedItemsBannerService.showBanner$ | async)
"
type="info"
>
<p>
{{ "unassignedItemsBanner" | i18n }}
<a
href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console"
bitLink
linkType="contrast"
target="_blank"
rel="noreferrer"
>{{ "learnMore" | i18n }}</a
>
</p>
<button <button
type="button" type="button"
class="btn primary callout-half" class="btn primary callout-half"
appStopClick appStopClick
(click)="dismissCallout()" (click)="unassignedItemsBannerService.hideBanner()"
> >
{{ "gotIt" | i18n }} {{ "gotIt" | i18n }}
</button> </button>
<button type="button" class="btn callout-half" appStopClick (click)="goToSettings()">
{{ "autofillSettings" | i18n }}
</button>
</app-callout> </app-callout>
<div class="box list" *ngIf="loginCiphers"> <div class="box list" *ngIf="loginCiphers">
<h2 class="box-header"> <h2 class="box-header">

View File

@ -1,13 +1,16 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { Subject, firstValueFrom } from "rxjs"; import { Subject, firstValueFrom, from } from "rxjs";
import { debounceTime, takeUntil } from "rxjs/operators"; import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -53,6 +56,11 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
private totpTimeout: number; private totpTimeout: number;
private loadedTimeout: number; private loadedTimeout: number;
private searchTimeout: number; private searchTimeout: number;
private initPageDetailsTimeout: number;
protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.UnassignedItemsBanner,
);
constructor( constructor(
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@ -70,6 +78,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService, private organizationService: OrganizationService,
private vaultFilterService: VaultFilterService, private vaultFilterService: VaultFilterService,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
private configService: ConfigService,
protected unassignedItemsBannerService: UnassignedItemsBannerService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -120,8 +130,14 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
} }
this.search$ this.search$
.pipe(debounceTime(500), takeUntil(this.destroy$)) .pipe(
.subscribe(() => this.searchVault()); debounceTime(500),
switchMap(() => {
return from(this.searchVault());
}),
takeUntil(this.destroy$),
)
.subscribe();
const autofillOnPageLoadOrgPolicy = await firstValueFrom( const autofillOnPageLoadOrgPolicy = await firstValueFrom(
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$, this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$,
@ -232,14 +248,12 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
} }
} }
searchVault() { async searchVault() {
if (!this.searchService.isSearchable(this.searchText)) { if (!(await this.searchService.isSearchable(this.searchText))) {
return; return;
} }
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
} }
closeOnEsc(e: KeyboardEvent) { closeOnEsc(e: KeyboardEvent) {
@ -303,18 +317,13 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
}); });
if (this.loginCiphers.length) { if (this.loginCiphers.length) {
void BrowserApi.tabSendMessage(this.tab, {
command: "collectPageDetails",
tab: this.tab,
sender: BroadcasterSubscriptionId,
});
this.loginCiphers = this.loginCiphers.sort((a, b) => this.loginCiphers = this.loginCiphers.sort((a, b) =>
this.cipherService.sortCiphersByLastUsedThenName(a, b), this.cipherService.sortCiphersByLastUsedThenName(a, b),
); );
} }
this.isLoading = this.loaded = true; this.isLoading = this.loaded = true;
this.collectTabPageDetails();
} }
async goToSettings() { async goToSettings() {
@ -352,4 +361,19 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand"); this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand");
} }
} }
private collectTabPageDetails() {
void BrowserApi.tabSendMessage(this.tab, {
command: "collectPageDetails",
tab: this.tab,
sender: BroadcasterSubscriptionId,
});
window.clearTimeout(this.initPageDetailsTimeout);
this.initPageDetailsTimeout = window.setTimeout(() => {
if (this.pageDetails.length === 0) {
this.collectTabPageDetails();
}
}, 250);
}
} }

View File

@ -1,8 +1,8 @@
import { Location } from "@angular/common"; import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
import { first } from "rxjs/operators"; import { first, switchMap, takeUntil } from "rxjs/operators";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
@ -53,7 +53,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
folderCounts = new Map<string, number>(); folderCounts = new Map<string, number>();
collectionCounts = new Map<string, number>(); collectionCounts = new Map<string, number>();
typeCounts = new Map<CipherType, number>(); typeCounts = new Map<CipherType, number>();
searchText: string;
state: BrowserGroupingsComponentState; state: BrowserGroupingsComponentState;
showLeftHeader = true; showLeftHeader = true;
searchPending = false; searchPending = false;
@ -71,6 +70,16 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
private hasSearched = false; private hasSearched = false;
private hasLoadedAllCiphers = false; private hasLoadedAllCiphers = false;
private allCiphers: CipherView[] = null; private allCiphers: CipherView[] = null;
private destroy$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>("");
private isSearchable: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
@ -148,6 +157,15 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY); BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
} }
}); });
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -161,6 +179,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.saveState(); this.saveState();
this.broadcasterService.unsubscribe(ComponentId); this.broadcasterService.unsubscribe(ComponentId);
this.destroy$.next();
this.destroy$.complete();
} }
async load() { async load() {
@ -181,7 +201,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
async loadCiphers() { async loadCiphers() {
this.allCiphers = await this.cipherService.getAllDecrypted(); this.allCiphers = await this.cipherService.getAllDecrypted();
if (!this.hasLoadedAllCiphers) { if (!this.hasLoadedAllCiphers) {
this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText));
} }
await this.search(null); await this.search(null);
this.getCounts(); this.getCounts();
@ -210,7 +230,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
} }
const filterDeleted = (c: CipherView) => !c.isDeleted; const filterDeleted = (c: CipherView) => !c.isDeleted;
if (timeout == null) { if (timeout == null) {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = this.isSearchable;
this.ciphers = await this.searchService.searchCiphers( this.ciphers = await this.searchService.searchCiphers(
this.searchText, this.searchText,
filterDeleted, filterDeleted,
@ -223,7 +243,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
} }
this.searchPending = true; this.searchPending = true;
this.searchTimeout = setTimeout(async () => { this.searchTimeout = setTimeout(async () => {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = this.isSearchable;
if (!this.hasLoadedAllCiphers && !this.hasSearched) { if (!this.hasLoadedAllCiphers && !this.hasSearched) {
await this.loadCiphers(); await this.loadCiphers();
} else { } else {
@ -381,9 +401,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
} }
showSearching() { showSearching() {
return ( return this.hasSearched || (!this.searchPending && this.isSearchable);
this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText))
);
} }
closeOnEsc(e: KeyboardEvent) { closeOnEsc(e: KeyboardEvent) {

View File

@ -20,7 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
@ -74,7 +74,7 @@ export class ViewComponent extends BaseViewComponent {
constructor( constructor(
cipherService: CipherService, cipherService: CipherService,
folderService: FolderService, folderService: FolderService,
totpService: TotpService, totpService: TotpServiceAbstraction,
tokenService: TokenService, tokenService: TokenService,
i18nService: I18nService, i18nService: I18nService,
cryptoService: CryptoService, cryptoService: CryptoService,

View File

@ -1,6 +1,10 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -18,6 +22,8 @@ import { CliUtils } from "../../utils";
export class UnlockCommand { export class UnlockCommand {
constructor( constructor(
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private stateService: StateService, private stateService: StateService,
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,
@ -45,11 +51,14 @@ export class UnlockCommand {
const kdf = await this.stateService.getKdfType(); const kdf = await this.stateService.getKdfType();
const kdfConfig = await this.stateService.getKdfConfig(); const kdfConfig = await this.stateService.getKdfConfig();
const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig); const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig);
const storedKeyHash = await this.cryptoService.getMasterKeyHash(); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const storedMasterKeyHash = await firstValueFrom(
this.masterPasswordService.masterKeyHash$(userId),
);
let passwordValid = false; let passwordValid = false;
if (masterKey != null) { if (masterKey != null) {
if (storedKeyHash != null) { if (storedMasterKeyHash != null) {
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, masterKey); passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, masterKey);
} else { } else {
const serverKeyHash = await this.cryptoService.hashMasterKey( const serverKeyHash = await this.cryptoService.hashMasterKey(
@ -67,7 +76,7 @@ export class UnlockCommand {
masterKey, masterKey,
HashPurpose.LocalAuthorization, HashPurpose.LocalAuthorization,
); );
await this.cryptoService.setMasterKeyHash(localKeyHash); await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
} catch { } catch {
// Ignore // Ignore
} }
@ -75,7 +84,7 @@ export class UnlockCommand {
} }
if (passwordValid) { if (passwordValid) {
await this.cryptoService.setMasterKey(masterKey); await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey);

View File

@ -28,6 +28,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
@ -168,6 +169,7 @@ export class Main {
organizationUserService: OrganizationUserService; organizationUserService: OrganizationUserService;
collectionService: CollectionService; collectionService: CollectionService;
vaultTimeoutService: VaultTimeoutService; vaultTimeoutService: VaultTimeoutService;
masterPasswordService: InternalMasterPasswordServiceAbstraction;
vaultTimeoutSettingsService: VaultTimeoutSettingsService; vaultTimeoutSettingsService: VaultTimeoutSettingsService;
syncService: SyncService; syncService: SyncService;
eventCollectionService: EventCollectionServiceAbstraction; eventCollectionService: EventCollectionServiceAbstraction;
@ -352,6 +354,7 @@ export class Main {
); );
this.cryptoService = new CryptoService( this.cryptoService = new CryptoService(
this.masterPasswordService,
this.keyGenerationService, this.keyGenerationService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.encryptService, this.encryptService,
@ -411,7 +414,7 @@ export class Main {
this.sendService, this.sendService,
); );
this.searchService = new SearchService(this.logService, this.i18nService); this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider);
this.broadcasterService = new BroadcasterService(); this.broadcasterService = new BroadcasterService();
@ -432,6 +435,8 @@ export class Main {
this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.policyApiService = new PolicyApiService(this.policyService, this.apiService);
this.keyConnectorService = new KeyConnectorService( this.keyConnectorService = new KeyConnectorService(
this.accountService,
this.masterPasswordService,
this.cryptoService, this.cryptoService,
this.apiService, this.apiService,
this.tokenService, this.tokenService,
@ -471,9 +476,10 @@ export class Main {
this.authRequestService = new AuthRequestService( this.authRequestService = new AuthRequestService(
this.appIdService, this.appIdService,
this.accountService,
this.masterPasswordService,
this.cryptoService, this.cryptoService,
this.apiService, this.apiService,
this.stateService,
); );
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
@ -481,6 +487,8 @@ export class Main {
); );
this.loginStrategyService = new LoginStrategyService( this.loginStrategyService = new LoginStrategyService(
this.accountService,
this.masterPasswordService,
this.cryptoService, this.cryptoService,
this.apiService, this.apiService,
this.tokenService, this.tokenService,
@ -568,6 +576,8 @@ export class Main {
this.userVerificationService = new UserVerificationService( this.userVerificationService = new UserVerificationService(
this.stateService, this.stateService,
this.cryptoService, this.cryptoService,
this.accountService,
this.masterPasswordService,
this.i18nService, this.i18nService,
this.userVerificationApiService, this.userVerificationApiService,
this.userDecryptionOptionsService, this.userDecryptionOptionsService,
@ -578,6 +588,8 @@ export class Main {
); );
this.vaultTimeoutService = new VaultTimeoutService( this.vaultTimeoutService = new VaultTimeoutService(
this.accountService,
this.masterPasswordService,
this.cipherService, this.cipherService,
this.folderService, this.folderService,
this.collectionService, this.collectionService,
@ -596,6 +608,8 @@ export class Main {
this.avatarService = new AvatarService(this.apiService, this.stateProvider); this.avatarService = new AvatarService(this.apiService, this.stateProvider);
this.syncService = new SyncService( this.syncService = new SyncService(
this.masterPasswordService,
this.accountService,
this.apiService, this.apiService,
this.domainSettingsService, this.domainSettingsService,
this.folderService, this.folderService,

View File

@ -122,6 +122,8 @@ export class ServeCommand {
this.shareCommand = new ShareCommand(this.main.cipherService); this.shareCommand = new ShareCommand(this.main.cipherService);
this.lockCommand = new LockCommand(this.main.vaultTimeoutService); this.lockCommand = new LockCommand(this.main.vaultTimeoutService);
this.unlockCommand = new UnlockCommand( this.unlockCommand = new UnlockCommand(
this.main.accountService,
this.main.masterPasswordService,
this.main.cryptoService, this.main.cryptoService,
this.main.stateService, this.main.stateService,
this.main.cryptoFunctionService, this.main.cryptoFunctionService,

View File

@ -253,6 +253,8 @@ export class Program {
if (!cmd.check) { if (!cmd.check) {
await this.exitIfNotAuthed(); await this.exitIfNotAuthed();
const command = new UnlockCommand( const command = new UnlockCommand(
this.main.accountService,
this.main.masterPasswordService,
this.main.cryptoService, this.main.cryptoService,
this.main.stateService, this.main.stateService,
this.main.cryptoFunctionService, this.main.cryptoFunctionService,
@ -613,6 +615,8 @@ export class Program {
this.processResponse(response, true); this.processResponse(response, true);
} else { } else {
const command = new UnlockCommand( const command = new UnlockCommand(
this.main.accountService,
this.main.masterPasswordService,
this.main.cryptoService, this.main.cryptoService,
this.main.stateService, this.main.stateService,
this.main.cryptoFunctionService, this.main.cryptoFunctionService,

View File

@ -1237,18 +1237,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.51" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.51" version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -24,7 +24,7 @@ rand = "=0.8.5"
retry = "=2.0.0" retry = "=2.0.0"
scopeguard = "=1.2.0" scopeguard = "=1.2.0"
sha2 = "=0.10.8" sha2 = "=0.10.8"
thiserror = "=1.0.51" thiserror = "=1.0.58"
typenum = "=1.17.0" typenum = "=1.17.0"
[build-dependencies] [build-dependencies]

View File

@ -203,7 +203,7 @@
"si", "si",
"sk", "sk",
"sl", "sl",
"sr", "sr-cyrl",
"sv", "sv",
"te", "te",
"th", "th",
@ -228,6 +228,7 @@
"artifactName": "${productName}-${version}-${arch}.${ext}" "artifactName": "${productName}-${version}-${arch}.${ext}"
}, },
"snap": { "snap": {
"summary": "After installation enable required `password-manager-service` by running `sudo snap connect bitwarden:password-manager-service`.",
"autoStart": true, "autoStart": true,
"base": "core22", "base": "core22",
"confinement": "strict", "confinement": "strict",

View File

@ -1,7 +1,7 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.3.2", "version": "2024.4.1",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",

View File

@ -26,6 +26,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@ -120,6 +121,7 @@ export class AppComponent implements OnInit, OnDestroy {
private accountCleanUpInProgress: { [userId: string]: boolean } = {}; private accountCleanUpInProgress: { [userId: string]: boolean } = {};
constructor( constructor(
private masterPasswordService: MasterPasswordServiceAbstraction,
private broadcasterService: BroadcasterService, private broadcasterService: BroadcasterService,
private folderService: InternalFolderService, private folderService: InternalFolderService,
private syncService: SyncService, private syncService: SyncService,
@ -408,8 +410,9 @@ export class AppComponent implements OnInit, OnDestroy {
(await this.authService.getAuthStatus(message.userId)) === (await this.authService.getAuthStatus(message.userId)) ===
AuthenticationStatus.Locked; AuthenticationStatus.Locked;
const forcedPasswordReset = const forcedPasswordReset =
(await this.stateService.getForceSetPasswordReason({ userId: message.userId })) != (await firstValueFrom(
ForceSetPasswordReason.None; this.masterPasswordService.forceSetPasswordReason$(message.userId),
)) != ForceSetPasswordReason.None;
if (locked) { if (locked) {
this.messagingService.send("locked", { userId: message.userId }); this.messagingService.send("locked", { userId: message.userId });
} else if (forcedPasswordReset) { } else if (forcedPasswordReset) {
@ -606,7 +609,7 @@ export class AppComponent implements OnInit, OnDestroy {
// This must come last otherwise the logout will prematurely trigger // This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up // a process reload before all the state service user data can be cleaned up
if (userBeingLoggedOut === preLogoutActiveUserId) { if (userBeingLoggedOut === preLogoutActiveUserId) {
this.searchService.clearIndex(); await this.searchService.clearIndex();
this.authService.logOut(async () => { this.authService.logOut(async () => {
if (expired) { if (expired) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(

View File

@ -20,6 +20,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@ -228,6 +229,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction, provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService, useClass: ElectronCryptoService,
deps: [ deps: [
InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction, KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
EncryptService, EncryptService,

View File

@ -14,7 +14,9 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -52,6 +54,7 @@ describe("LockComponent", () => {
let broadcasterServiceMock: MockProxy<BroadcasterService>; let broadcasterServiceMock: MockProxy<BroadcasterService>;
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>; let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
let activatedRouteMock: MockProxy<ActivatedRoute>; let activatedRouteMock: MockProxy<ActivatedRoute>;
let mockMasterPasswordService: FakeMasterPasswordService;
const mockUserId = Utils.newGuid() as UserId; const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
@ -67,6 +70,8 @@ describe("LockComponent", () => {
activatedRouteMock = mock<ActivatedRoute>(); activatedRouteMock = mock<ActivatedRoute>();
activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>(); activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>();
mockMasterPasswordService = new FakeMasterPasswordService();
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptAutomatically$ = of(false);
biometricStateService.promptCancelled$ = of(false); biometricStateService.promptCancelled$ = of(false);
@ -74,6 +79,7 @@ describe("LockComponent", () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [LockComponent, I18nPipe], declarations: [LockComponent, I18nPipe],
providers: [ providers: [
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
{ {
provide: I18nService, provide: I18nService,
useValue: mock<I18nService>(), useValue: mock<I18nService>(),

View File

@ -11,6 +11,7 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums"; import { DeviceType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@ -38,6 +39,7 @@ export class LockComponent extends BaseLockComponent {
private autoPromptBiometric = false; private autoPromptBiometric = false;
constructor( constructor(
masterPasswordService: InternalMasterPasswordServiceAbstraction,
router: Router, router: Router,
i18nService: I18nService, i18nService: I18nService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
@ -63,6 +65,7 @@ export class LockComponent extends BaseLockComponent {
accountService: AccountService, accountService: AccountService,
) { ) {
super( super(
masterPasswordService,
router, router,
i18nService, i18nService,
platformUtilsService, platformUtilsService,

View File

@ -8,6 +8,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -29,6 +31,8 @@ const BroadcasterSubscriptionId = "SetPasswordComponent";
}) })
export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy { export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy {
constructor( constructor(
accountService: AccountService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
apiService: ApiService, apiService: ApiService,
i18nService: I18nService, i18nService: I18nService,
cryptoService: CryptoService, cryptoService: CryptoService,
@ -50,6 +54,8 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
dialogService: DialogService, dialogService: DialogService,
) { ) {
super( super(
accountService,
masterPasswordService,
i18nService, i18nService,
cryptoService, cryptoService,
messagingService, messagingService,

View File

@ -7,6 +7,8 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -39,6 +41,8 @@ export class SsoComponent extends BaseSsoComponent {
logService: LogService, logService: LogService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigService, configService: ConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
) { ) {
super( super(
ssoLoginService, ssoLoginService,
@ -55,6 +59,8 @@ export class SsoComponent extends BaseSsoComponent {
logService, logService,
userDecryptionOptionsService, userDecryptionOptionsService,
configService, configService,
masterPasswordService,
accountService,
); );
super.onSuccessfulLogin = async () => { super.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@ -11,6 +11,8 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@ -60,6 +62,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
configService: ConfigService, configService: ConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
@Inject(WINDOW) protected win: Window, @Inject(WINDOW) protected win: Window,
) { ) {
super( super(
@ -79,6 +83,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
userDecryptionOptionsService, userDecryptionOptionsService,
ssoLoginService, ssoLoginService,
configService, configService,
masterPasswordService,
accountService,
); );
super.onSuccessfulLogin = async () => { super.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@ -494,7 +494,7 @@
"message": "Kansio poistettiin" "message": "Kansio poistettiin"
}, },
"loginOrCreateNewAccount": { "loginOrCreateNewAccount": {
"message": "Käytä salattua holviasi kirjautumalla sisään tai tai luo uusi tili." "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili."
}, },
"createAccount": { "createAccount": {
"message": "Luo tili" "message": "Luo tili"
@ -2689,7 +2689,7 @@
"description": "Label indicating the most common import formats" "description": "Label indicating the most common import formats"
}, },
"troubleshooting": { "troubleshooting": {
"message": "Vianmääritys" "message": "Vianselvitys"
}, },
"disableHardwareAccelerationRestart": { "disableHardwareAccelerationRestart": {
"message": "Poista laitteistokiihdytys käytöstä ja käynnistä sovellus uudelleen" "message": "Poista laitteistokiihdytys käytöstä ja käynnistä sovellus uudelleen"

View File

@ -404,7 +404,7 @@
"message": "長度" "message": "長度"
}, },
"passwordMinLength": { "passwordMinLength": {
"message": "Minimum password length" "message": "最小密碼長度"
}, },
"uppercase": { "uppercase": {
"message": "大寫 (A-Z)" "message": "大寫 (A-Z)"
@ -561,10 +561,10 @@
"message": "帳戶已建立!現在可以登入了。" "message": "帳戶已建立!現在可以登入了。"
}, },
"youSuccessfullyLoggedIn": { "youSuccessfullyLoggedIn": {
"message": "You successfully logged in" "message": "你已成功登入"
}, },
"youMayCloseThisWindow": { "youMayCloseThisWindow": {
"message": "You may close this window" "message": "你可以關閉此視窗"
}, },
"masterPassSent": { "masterPassSent": {
"message": "已寄出包含您主密碼提示的電子郵件。" "message": "已寄出包含您主密碼提示的電子郵件。"
@ -1546,15 +1546,15 @@
"message": "設定主密碼" "message": "設定主密碼"
}, },
"orgPermissionsUpdatedMustSetPassword": { "orgPermissionsUpdatedMustSetPassword": {
"message": "Your organization permissions were updated, requiring you to set a master password.", "message": "你的組織權限已更新,要求你設定一個主密碼。",
"description": "Used as a card title description on the set password page to explain why the user is there" "description": "Used as a card title description on the set password page to explain why the user is there"
}, },
"orgRequiresYouToSetPassword": { "orgRequiresYouToSetPassword": {
"message": "Your organization requires you to set a master password.", "message": "你的組織要求你設定一個主密碼。",
"description": "Used as a card title description on the set password page to explain why the user is there" "description": "Used as a card title description on the set password page to explain why the user is there"
}, },
"verificationRequired": { "verificationRequired": {
"message": "Verification required", "message": "需要驗證",
"description": "Default title for the user verification dialog." "description": "Default title for the user verification dialog."
}, },
"currentMasterPass": { "currentMasterPass": {
@ -1645,10 +1645,10 @@
"message": "在您的桌面和瀏覽器閒建立連綫時,透過要求指紋短語確認,以添加一個額外的安全層。每次建立連綫都需要使用者干預和驗證。" "message": "在您的桌面和瀏覽器閒建立連綫時,透過要求指紋短語確認,以添加一個額外的安全層。每次建立連綫都需要使用者干預和驗證。"
}, },
"enableHardwareAcceleration": { "enableHardwareAcceleration": {
"message": "Use hardware acceleration" "message": "使用硬體加速"
}, },
"enableHardwareAccelerationDesc": { "enableHardwareAccelerationDesc": {
"message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." "message": "此設定預設為開啟。僅當你遇到圖形問題時才關閉。需要重新啟動。"
}, },
"approve": { "approve": {
"message": "核准" "message": "核准"
@ -1889,40 +1889,40 @@
"message": "您的主密碼不符合一個或多個組織原則要求。您必須立即更新您的主密碼才能存取密碼庫。進行此動作將登出您目前的工作階段,需要您重新登入。其他裝置上的工作階段可能繼續長達一小時。" "message": "您的主密碼不符合一個或多個組織原則要求。您必須立即更新您的主密碼才能存取密碼庫。進行此動作將登出您目前的工作階段,需要您重新登入。其他裝置上的工作階段可能繼續長達一小時。"
}, },
"tryAgain": { "tryAgain": {
"message": "Try again" "message": "再試一次"
}, },
"verificationRequiredForActionSetPinToContinue": { "verificationRequiredForActionSetPinToContinue": {
"message": "Verification required for this action. Set a PIN to continue." "message": "此操作需要驗證。設定 PIN 碼以繼續。"
}, },
"setPin": { "setPin": {
"message": "Set PIN" "message": "設定 PIN 碼"
}, },
"verifyWithBiometrics": { "verifyWithBiometrics": {
"message": "Verify with biometrics" "message": "使用生物辨識進行驗證"
}, },
"awaitingConfirmation": { "awaitingConfirmation": {
"message": "Awaiting confirmation" "message": "正在等待確認"
}, },
"couldNotCompleteBiometrics": { "couldNotCompleteBiometrics": {
"message": "Could not complete biometrics." "message": "無法完成生物辨識。"
}, },
"needADifferentMethod": { "needADifferentMethod": {
"message": "Need a different method?" "message": "需要不同的方法嗎?"
}, },
"useMasterPassword": { "useMasterPassword": {
"message": "Use master password" "message": "使用主密碼"
}, },
"usePin": { "usePin": {
"message": "Use PIN" "message": "使用 PIN 碼"
}, },
"useBiometrics": { "useBiometrics": {
"message": "Use biometrics" "message": "使用生物辨識"
}, },
"enterVerificationCodeSentToEmail": { "enterVerificationCodeSentToEmail": {
"message": "Enter the verification code that was sent to your email." "message": "Enter the verification code that was sent to your email."
}, },
"resendCode": { "resendCode": {
"message": "Resend code" "message": "重新傳送驗證碼"
}, },
"hours": { "hours": {
"message": "小時" "message": "小時"
@ -2465,7 +2465,7 @@
"message": "全部清除" "message": "全部清除"
}, },
"plusNMore": { "plusNMore": {
"message": "+ $QUANTITY$ more", "message": "+ $QUANTITY$ 個更多",
"placeholders": { "placeholders": {
"quantity": { "quantity": {
"content": "$1", "content": "$1",
@ -2477,7 +2477,7 @@
"message": "子選單" "message": "子選單"
}, },
"skipToContent": { "skipToContent": {
"message": "Skip to content" "message": "跳至內容"
}, },
"typePasskey": { "typePasskey": {
"message": "密碼金鑰" "message": "密碼金鑰"
@ -2621,13 +2621,13 @@
"message": "使用者名稱或密碼不正確" "message": "使用者名稱或密碼不正確"
}, },
"incorrectPassword": { "incorrectPassword": {
"message": "Incorrect password" "message": "密碼不正確"
}, },
"incorrectCode": { "incorrectCode": {
"message": "Incorrect code" "message": "驗證碼不正確"
}, },
"incorrectPin": { "incorrectPin": {
"message": "Incorrect PIN" "message": "PIN 碼不正確"
}, },
"multifactorAuthenticationFailed": { "multifactorAuthenticationFailed": {
"message": "多因素驗證失敗" "message": "多因素驗證失敗"
@ -2685,22 +2685,22 @@
"message": "將與您的 LastPass 帳戶關聯的 YubiKey 插入電腦的 USB 連接埠,然後觸摸其按鈕。" "message": "將與您的 LastPass 帳戶關聯的 YubiKey 插入電腦的 USB 連接埠,然後觸摸其按鈕。"
}, },
"commonImportFormats": { "commonImportFormats": {
"message": "Common formats", "message": "常見格式",
"description": "Label indicating the most common import formats" "description": "Label indicating the most common import formats"
}, },
"troubleshooting": { "troubleshooting": {
"message": "Troubleshooting" "message": "疑難排解"
}, },
"disableHardwareAccelerationRestart": { "disableHardwareAccelerationRestart": {
"message": "Disable hardware acceleration and restart" "message": "停用硬體加速並重新啟動"
}, },
"enableHardwareAccelerationRestart": { "enableHardwareAccelerationRestart": {
"message": "Enable hardware acceleration and restart" "message": "啟用硬體加速並重新啟動"
}, },
"removePasskey": { "removePasskey": {
"message": "Remove passkey" "message": "移除金鑰"
}, },
"passkeyRemoved": { "passkeyRemoved": {
"message": "Passkey removed" "message": "金鑰已移除"
} }
} }

View File

@ -1,12 +1,12 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.3.2", "version": "2024.4.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.3.2", "version": "2024.4.1",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@bitwarden/desktop-native": "file:../desktop_native", "@bitwarden/desktop-native": "file:../desktop_native",

View File

@ -2,7 +2,7 @@
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"productName": "Bitwarden", "productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.3.2", "version": "2024.4.1",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com", "homepage": "https://bitwarden.com",
"license": "GPL-3.0", "license": "GPL-3.0",

View File

@ -1,6 +1,7 @@
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -30,6 +31,7 @@ describe("electronCryptoService", () => {
const platformUtilService = mock<PlatformUtilsService>(); const platformUtilService = mock<PlatformUtilsService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const stateService = mock<StateService>(); const stateService = mock<StateService>();
let masterPasswordService: FakeMasterPasswordService;
let accountService: FakeAccountService; let accountService: FakeAccountService;
let stateProvider: FakeStateProvider; let stateProvider: FakeStateProvider;
const biometricStateService = mock<BiometricStateService>(); const biometricStateService = mock<BiometricStateService>();
@ -38,9 +40,11 @@ describe("electronCryptoService", () => {
beforeEach(() => { beforeEach(() => {
accountService = mockAccountServiceWith("userId" as UserId); accountService = mockAccountServiceWith("userId" as UserId);
masterPasswordService = new FakeMasterPasswordService();
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
sut = new ElectronCryptoService( sut = new ElectronCryptoService(
masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
encryptService, encryptService,

View File

@ -1,6 +1,7 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -20,6 +21,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
export class ElectronCryptoService extends CryptoService { export class ElectronCryptoService extends CryptoService {
constructor( constructor(
masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService, keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
encryptService: EncryptService, encryptService: EncryptService,
@ -31,6 +33,7 @@ export class ElectronCryptoService extends CryptoService {
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
) { ) {
super( super(
masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
encryptService, encryptService,
@ -159,12 +162,16 @@ export class ElectronCryptoService extends CryptoService {
const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId }); const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId });
// decrypt // decrypt
const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey; const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey;
let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
encUserKey = encUserKey ?? (await this.stateService.getMasterKeyEncryptedUserKey()); const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey();
const encUserKey =
encUserKeyPrim != null
? new EncString(encUserKeyPrim)
: await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId);
if (!encUserKey) { if (!encUserKey) {
throw new Error("No user key found during biometric migration"); throw new Error("No user key found during biometric migration");
} }
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); const userKey = await this.decryptUserKeyWithMasterKey(masterKey, encUserKey);
// migrate // migrate
await this.storeBiometricKey(userKey, userId); await this.storeBiometricKey(userKey, userId);
await this.stateService.setCryptoMasterKeyBiometric(null, { userId }); await this.stateService.setCryptoMasterKeyBiometric(null, { userId });

View File

@ -1,6 +1,7 @@
import { Injectable, NgZone } from "@angular/core"; import { Injectable, NgZone } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -30,6 +31,7 @@ export class NativeMessagingService {
private sharedSecrets = new Map<string, SymmetricCryptoKey>(); private sharedSecrets = new Map<string, SymmetricCryptoKey>();
constructor( constructor(
private masterPasswordService: MasterPasswordServiceAbstraction,
private cryptoFunctionService: CryptoFunctionService, private cryptoFunctionService: CryptoFunctionService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private platformUtilService: PlatformUtilsService, private platformUtilService: PlatformUtilsService,
@ -162,7 +164,9 @@ export class NativeMessagingService {
KeySuffixOptions.Biometric, KeySuffixOptions.Biometric,
message.userId, message.userId,
); );
const masterKey = await this.cryptoService.getMasterKey(message.userId); const masterKey = await firstValueFrom(
this.masterPasswordService.masterKey$(message.userId as UserId),
);
if (userKey != null) { if (userKey != null) {
// we send the master key still for backwards compatibility // we send the master key still for backwards compatibility

View File

@ -1,5 +1,5 @@
import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; import { Directive, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
@ -33,7 +33,9 @@ const MaxCheckedCount = 500;
@Directive() @Directive()
export abstract class BasePeopleComponent< export abstract class BasePeopleComponent<
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView, UserType extends ProviderUserUserDetailsResponse | OrganizationUserView,
> { >
implements OnInit, OnDestroy
{
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
confirmModalRef: ViewContainerRef; confirmModalRef: ViewContainerRef;
@ -88,7 +90,6 @@ export abstract class BasePeopleComponent<
status: StatusType; status: StatusType;
users: UserType[] = []; users: UserType[] = [];
pagedUsers: UserType[] = []; pagedUsers: UserType[] = [];
searchText: string;
actionPromise: Promise<void>; actionPromise: Promise<void>;
protected allUsers: UserType[] = []; protected allUsers: UserType[] = [];
@ -97,7 +98,19 @@ export abstract class BasePeopleComponent<
protected didScroll = false; protected didScroll = false;
protected pageSize = 100; protected pageSize = 100;
protected destroy$ = new Subject<void>();
private pagedUsersCount = 0; private pagedUsersCount = 0;
private _searchText$ = new BehaviorSubject<string>("");
private isSearching: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor( constructor(
protected apiService: ApiService, protected apiService: ApiService,
@ -122,6 +135,22 @@ export abstract class BasePeopleComponent<
abstract reinviteUser(id: string): Promise<void>; abstract reinviteUser(id: string): Promise<void>;
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>; abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<void>;
ngOnInit(): void {
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearching = isSearchable;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async load() { async load() {
const response = await this.getUsers(); const response = await this.getUsers();
this.statusMap.clear(); this.statusMap.clear();
@ -390,12 +419,8 @@ export abstract class BasePeopleComponent<
} }
} }
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
isPaging() { isPaging() {
const searching = this.isSearching(); const searching = this.isSearching;
if (searching && this.didScroll) { if (searching && this.didScroll) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -91,15 +91,16 @@ export class GroupsComponent implements OnInit, OnDestroy {
private pagedGroupsCount = 0; private pagedGroupsCount = 0;
private pagedGroups: GroupDetailsRow[]; private pagedGroups: GroupDetailsRow[];
private searchedGroups: GroupDetailsRow[]; private searchedGroups: GroupDetailsRow[];
private _searchText: string; private _searchText$ = new BehaviorSubject<string>("");
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private refreshGroups$ = new BehaviorSubject<void>(null); private refreshGroups$ = new BehaviorSubject<void>(null);
private isSearching: boolean = false;
get searchText() { get searchText() {
return this._searchText; return this._searchText$.value;
} }
set searchText(value: string) { set searchText(value: string) {
this._searchText = value; this._searchText$.next(value);
// Manually update as we are not using the search pipe in the template // Manually update as we are not using the search pipe in the template
this.updateSearchedGroups(); this.updateSearchedGroups();
} }
@ -114,7 +115,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
if (this.isPaging()) { if (this.isPaging()) {
return this.pagedGroups; return this.pagedGroups;
} }
if (this.isSearching()) { if (this.isSearching) {
return this.searchedGroups; return this.searchedGroups;
} }
return this.groups; return this.groups;
@ -180,6 +181,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
.subscribe(); .subscribe();
this._searchText$
.pipe(
switchMap((searchText) => this.searchService.isSearchable(searchText)),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearching = isSearchable;
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -297,10 +307,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.loadMore(); this.loadMore();
} }
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
check(groupRow: GroupDetailsRow) { check(groupRow: GroupDetailsRow) {
groupRow.checked = !groupRow.checked; groupRow.checked = !groupRow.checked;
} }
@ -310,7 +316,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
} }
isPaging() { isPaging() {
const searching = this.isSearching(); const searching = this.isSearching;
if (searching && this.didScroll) { if (searching && this.didScroll) {
this.resetPaging(); this.resetPaging();
} }
@ -340,7 +346,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
} }
private updateSearchedGroups() { private updateSearchedGroups() {
if (this.searchService.isSearchable(this.searchText)) { if (this.isSearching) {
// Making use of the pipe in the component as we need know which groups where filtered // Making use of the pipe in the component as we need know which groups where filtered
this.searchedGroups = this.searchPipe.transform( this.searchedGroups = this.searchPipe.transform(
this.groups, this.groups,

View File

@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { import {
combineLatest, combineLatest,
@ -9,7 +9,6 @@ import {
map, map,
Observable, Observable,
shareReplay, shareReplay,
Subject,
switchMap, switchMap,
takeUntil, takeUntil,
} from "rxjs"; } from "rxjs";
@ -73,10 +72,7 @@ import { ResetPasswordComponent } from "./components/reset-password.component";
selector: "app-org-people", selector: "app-org-people",
templateUrl: "people.component.html", templateUrl: "people.component.html",
}) })
export class PeopleComponent export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
extends BasePeopleComponent<OrganizationUserView>
implements OnInit, OnDestroy
{
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef; groupsModalRef: ViewContainerRef;
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
@ -99,7 +95,6 @@ export class PeopleComponent
orgResetPasswordPolicyEnabled = false; orgResetPasswordPolicyEnabled = false;
protected canUseSecretsManager$: Observable<boolean>; protected canUseSecretsManager$: Observable<boolean>;
private destroy$ = new Subject<void>();
constructor( constructor(
apiService: ApiService, apiService: ApiService,
@ -210,8 +205,7 @@ export class PeopleComponent
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); super.ngOnDestroy();
this.destroy$.complete();
} }
async load() { async load() {

View File

@ -281,7 +281,7 @@ export class AppComponent implements OnDestroy, OnInit {
await this.stateEventRunnerService.handleEvent("logout", userId as UserId); await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
this.searchService.clearIndex(); await this.searchService.clearIndex();
this.authService.logOut(async () => { this.authService.logOut(async () => {
if (expired) { if (expired) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(

View File

@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -9,7 +10,6 @@ import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { Send } from "@bitwarden/common/tools/send/models/domain/send"; import { Send } from "@bitwarden/common/tools/send/models/domain/send";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@ -22,6 +22,10 @@ import { Folder } from "@bitwarden/common/vault/models/domain/folder";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
FakeAccountService,
mockAccountServiceWith,
} from "../../../../../../libs/common/spec/fake-account-service";
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
import { StateService } from "../../core"; import { StateService } from "../../core";
import { EmergencyAccessService } from "../emergency-access"; import { EmergencyAccessService } from "../emergency-access";
@ -46,8 +50,10 @@ describe("KeyRotationService", () => {
const mockUserId = Utils.newGuid() as UserId; const mockUserId = Utils.newGuid() as UserId;
const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId);
let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService();
beforeAll(() => { beforeAll(() => {
mockMasterPasswordService = new FakeMasterPasswordService();
mockApiService = mock<UserKeyRotationApiService>(); mockApiService = mock<UserKeyRotationApiService>();
mockCipherService = mock<CipherService>(); mockCipherService = mock<CipherService>();
mockFolderService = mock<FolderService>(); mockFolderService = mock<FolderService>();
@ -61,6 +67,7 @@ describe("KeyRotationService", () => {
mockConfigService = mock<ConfigService>(); mockConfigService = mock<ConfigService>();
keyRotationService = new UserKeyRotationService( keyRotationService = new UserKeyRotationService(
mockMasterPasswordService,
mockApiService, mockApiService,
mockCipherService, mockCipherService,
mockFolderService, mockFolderService,
@ -174,7 +181,10 @@ describe("KeyRotationService", () => {
it("saves the master key in state after creation", async () => { it("saves the master key in state after creation", async () => {
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"); await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword");
expect(mockCryptoService.setMasterKey).toHaveBeenCalledWith("mockMasterKey" as any); expect(mockMasterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(
"mockMasterKey" as any,
mockUserId,
);
}); });
it("uses legacy rotation if feature flag is off", async () => { it("uses legacy rotation if feature flag is off", async () => {

View File

@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -25,6 +26,7 @@ import { UserKeyRotationApiService } from "./user-key-rotation-api.service";
@Injectable() @Injectable()
export class UserKeyRotationService { export class UserKeyRotationService {
constructor( constructor(
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private apiService: UserKeyRotationApiService, private apiService: UserKeyRotationApiService,
private cipherService: CipherService, private cipherService: CipherService,
private folderService: FolderService, private folderService: FolderService,
@ -61,7 +63,8 @@ export class UserKeyRotationService {
} }
// Set master key again in case it was lost (could be lost on refresh) // Set master key again in case it was lost (could be lost on refresh)
await this.cryptoService.setMasterKey(masterKey); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setMasterKey(masterKey, userId);
const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey); const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey);
if (!newUserKey || !newEncUserKey) { if (!newUserKey || !newEncUserKey) {

View File

@ -1,80 +1,12 @@
import { Component, NgZone } from "@angular/core"; import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { DialogService } from "@bitwarden/components";
@Component({ @Component({
selector: "app-lock", selector: "app-lock",
templateUrl: "lock.component.html", templateUrl: "lock.component.html",
}) })
export class LockComponent extends BaseLockComponent { export class LockComponent extends BaseLockComponent {
constructor(
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
cryptoService: CryptoService,
vaultTimeoutService: VaultTimeoutService,
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
environmentService: EnvironmentService,
stateService: StateService,
apiService: ApiService,
logService: LogService,
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordStrengthService: PasswordStrengthServiceAbstraction,
dialogService: DialogService,
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
userVerificationService: UserVerificationService,
pinCryptoService: PinCryptoServiceAbstraction,
biometricStateService: BiometricStateService,
accountService: AccountService,
) {
super(
router,
i18nService,
platformUtilsService,
messagingService,
cryptoService,
vaultTimeoutService,
vaultTimeoutSettingsService,
environmentService,
stateService,
apiService,
logService,
ngZone,
policyApiService,
policyService,
passwordStrengthService,
dialogService,
deviceTrustCryptoService,
userVerificationService,
pinCryptoService,
biometricStateService,
accountService,
);
}
async ngOnInit() { async ngOnInit() {
await super.ngOnInit(); await super.ngOnInit();
this.onSuccessfulSubmit = async () => { this.onSuccessfulSubmit = async () => {

View File

@ -10,6 +10,8 @@ import {
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response"; import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { HttpStatusCode } from "@bitwarden/common/enums"; import { HttpStatusCode } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@ -46,6 +48,8 @@ export class SsoComponent extends BaseSsoComponent {
private validationService: ValidationService, private validationService: ValidationService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigService, configService: ConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
) { ) {
super( super(
ssoLoginService, ssoLoginService,
@ -62,6 +66,8 @@ export class SsoComponent extends BaseSsoComponent {
logService, logService,
userDecryptionOptionsService, userDecryptionOptionsService,
configService, configService,
masterPasswordService,
accountService,
); );
this.redirectUri = window.location.origin + "/sso-connector.html"; this.redirectUri = window.location.origin + "/sso-connector.html";
this.clientId = "web"; this.clientId = "web";

View File

@ -10,6 +10,8 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
@ -50,6 +52,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
ssoLoginService: SsoLoginServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction,
configService: ConfigService, configService: ConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
@Inject(WINDOW) protected win: Window, @Inject(WINDOW) protected win: Window,
) { ) {
super( super(
@ -69,6 +73,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest
userDecryptionOptionsService, userDecryptionOptionsService,
ssoLoginService, ssoLoginService,
configService, configService,
masterPasswordService,
accountService,
); );
this.onSuccessfulLoginNavigate = this.goAfterLogIn; this.onSuccessfulLoginNavigate = this.goAfterLogIn;
} }

View File

@ -279,10 +279,7 @@
/> />
</div> </div>
</div> </div>
<div <div class="col-6" *ngIf="showTaxIdCheckbox">
class="col-6"
*ngIf="organizationId && taxInfo.country !== 'US' && countrySupportsTax(taxInfo.country)"
>
<div class="form-group form-check"> <div class="form-group form-check">
<input <input
class="form-check-input" class="form-check-input"
@ -295,21 +292,22 @@
</div> </div>
</div> </div>
</div> </div>
<div <div class="row" *ngIf="showTaxIdFields">
class="row"
*ngIf="organizationId && taxInfo.includeTaxId && countrySupportsTax(taxInfo.country)"
>
<div class="col-6"> <div class="col-6">
<div class="form-group"> <div class="form-group">
<label for="taxId">{{ "taxIdNumber" | i18n }}</label> <label for="taxId">{{ "taxIdNumber" | i18n }}</label>
<input id="taxId" class="form-control" type="text" name="taxId" [(ngModel)]="taxInfo.taxId" /> <input
id="taxId"
class="form-control"
type="text"
name="taxId"
[(ngModel)]="taxInfo.taxId"
[required]="taxInfo.includeTaxId"
/>
</div> </div>
</div> </div>
</div> </div>
<div <div class="row" *ngIf="showTaxIdFields">
class="row"
*ngIf="organizationId && taxInfo.includeTaxId && countrySupportsTax(taxInfo.country)"
>
<div class="col-6"> <div class="col-6">
<div class="form-group"> <div class="form-group">
<label for="addressLine1">{{ "address1" | i18n }}</label> <label for="addressLine1">{{ "address1" | i18n }}</label>

View File

@ -3,7 +3,7 @@ import { ActivatedRoute } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/organization-tax-info-update.request"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { TaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/tax-info-update.request"; import { TaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/tax-info-update.request";
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
import { TaxRateResponse } from "@bitwarden/common/billing/models/response/tax-rate.response"; import { TaxRateResponse } from "@bitwarden/common/billing/models/response/tax-rate.response";
@ -29,6 +29,7 @@ export class TaxInfoComponent {
loading = true; loading = true;
organizationId: string; organizationId: string;
providerId: string;
taxInfo: TaxInfoView = { taxInfo: TaxInfoView = {
taxId: null, taxId: null,
line1: null, line1: null,
@ -61,6 +62,12 @@ export class TaxInfoComponent {
) {} ) {}
async ngOnInit() { async ngOnInit() {
// Provider setup
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.subscribe((params) => {
this.providerId = params.providerId;
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
@ -126,9 +133,25 @@ export class TaxInfoComponent {
} }
} }
get showTaxIdCheckbox() {
return (
(this.organizationId || this.providerId) &&
this.taxInfo.country !== "US" &&
this.countrySupportsTax(this.taxInfo.country)
);
}
get showTaxIdFields() {
return (
(this.organizationId || this.providerId) &&
this.taxInfo.includeTaxId &&
this.countrySupportsTax(this.taxInfo.country)
);
}
getTaxInfoRequest(): TaxInfoUpdateRequest { getTaxInfoRequest(): TaxInfoUpdateRequest {
if (this.organizationId) { if (this.organizationId || this.providerId) {
const request = new OrganizationTaxInfoUpdateRequest(); const request = new ExpandedTaxInfoUpdateRequest();
request.country = this.taxInfo.country; request.country = this.taxInfo.country;
request.postalCode = this.taxInfo.postalCode; request.postalCode = this.taxInfo.postalCode;
@ -164,7 +187,7 @@ export class TaxInfoComponent {
return this.organizationId return this.organizationId
? this.organizationApiService.updateTaxInfo( ? this.organizationApiService.updateTaxInfo(
this.organizationId, this.organizationId,
request as OrganizationTaxInfoUpdateRequest, request as ExpandedTaxInfoUpdateRequest,
) )
: this.apiService.putTaxInfo(request); : this.apiService.putTaxInfo(request);
} }

View File

@ -1,16 +1,18 @@
<bit-banner <bit-banner
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6" class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
(onClose)="webLayoutMigrationBannerService.hideBanner()" (onClose)="unassignedItemsBannerService.hideBanner()"
*ngIf="webLayoutMigrationBannerService.showBanner$ | async" *ngIf="
(unassignedItemsBannerEnabled$ | async) && (unassignedItemsBannerService.showBanner$ | async)
"
> >
{{ "newWebApp" | i18n }} {{ "unassignedItemsBanner" | i18n }}
<a <a
href="https://bitwarden.com/blog/bitwarden-design-updating-the-navigation-in-the-web-app" href="https://bitwarden.com/help/unassigned-vault-items-moved-to-admin-console"
bitLink bitLink
linkType="contrast" linkType="contrast"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>{{ "releaseBlog" | i18n }}</a >{{ "learnMore" | i18n }}</a
> >
</bit-banner> </bit-banner>
<header <header

View File

@ -2,15 +2,16 @@ import { Component, Input } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { combineLatest, map, Observable } from "rxjs"; import { combineLatest, map, Observable } from "rxjs";
import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { AccountProfile } from "@bitwarden/common/platform/models/domain/account";
import { WebLayoutMigrationBannerService } from "./web-layout-migration-banner.service";
@Component({ @Component({
selector: "app-header", selector: "app-header",
templateUrl: "./web-header.component.html", templateUrl: "./web-header.component.html",
@ -31,6 +32,9 @@ export class WebHeaderComponent {
protected canLock$: Observable<boolean>; protected canLock$: Observable<boolean>;
protected selfHosted: boolean; protected selfHosted: boolean;
protected hostname = location.hostname; protected hostname = location.hostname;
protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.UnassignedItemsBanner,
);
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -38,7 +42,8 @@ export class WebHeaderComponent {
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private messagingService: MessagingService, private messagingService: MessagingService,
protected webLayoutMigrationBannerService: WebLayoutMigrationBannerService, protected unassignedItemsBannerService: UnassignedItemsBannerService,
private configService: ConfigService,
) { ) {
this.routeData$ = this.route.data.pipe( this.routeData$ = this.route.data.pipe(
map((params) => { map((params) => {

View File

@ -272,7 +272,7 @@ export class VaultComponent implements OnInit, OnDestroy {
concatMap(async ([ciphers, filter, searchText]) => { concatMap(async ([ciphers, filter, searchText]) => {
const filterFunction = createFilterFunction(filter); const filterFunction = createFilterFunction(filter);
if (this.searchService.isSearchable(searchText)) { if (await this.searchService.isSearchable(searchText)) {
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers); return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
} }
@ -283,7 +283,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
filter(([collections, filter]) => collections != undefined && filter != undefined), filter(([collections, filter]) => collections != undefined && filter != undefined),
map(([collections, filter, searchText]) => { concatMap(async ([collections, filter, searchText]) => {
if (filter.collectionId === undefined || filter.collectionId === Unassigned) { if (filter.collectionId === undefined || filter.collectionId === Unassigned) {
return []; return [];
} }
@ -303,7 +303,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
} }
if (this.searchService.isSearchable(searchText)) { if (await this.searchService.isSearchable(searchText)) {
collectionsToReturn = this.searchPipe.transform( collectionsToReturn = this.searchPipe.transform(
collectionsToReturn, collectionsToReturn,
searchText, searchText,

View File

@ -331,7 +331,7 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
} }
this.searchService.indexCiphers(ciphers, organization.id); await this.searchService.indexCiphers(ciphers, organization.id);
return ciphers; return ciphers;
}), }),
); );
@ -350,7 +350,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
filter(([collections, filter]) => collections != undefined && filter != undefined), filter(([collections, filter]) => collections != undefined && filter != undefined),
map(([collections, filter, searchText]) => { concatMap(async ([collections, filter, searchText]) => {
if ( if (
filter.collectionId === Unassigned || filter.collectionId === Unassigned ||
(filter.collectionId === undefined && filter.type !== undefined) (filter.collectionId === undefined && filter.type !== undefined)
@ -369,7 +369,7 @@ export class VaultComponent implements OnInit, OnDestroy {
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
} }
if (this.searchService.isSearchable(searchText)) { if (await this.searchService.isSearchable(searchText)) {
collectionsToReturn = this.searchPipe.transform( collectionsToReturn = this.searchPipe.transform(
collectionsToReturn, collectionsToReturn,
searchText, searchText,
@ -436,7 +436,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const filterFunction = createFilterFunction(filter); const filterFunction = createFilterFunction(filter);
if (this.searchService.isSearchable(searchText)) { if (await this.searchService.isSearchable(searchText)) {
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers); return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
} }

View File

@ -7899,5 +7899,8 @@
}, },
"machineAccountAccessUpdated": { "machineAccountAccessUpdated": {
"message": "Machine account access updated" "message": "Machine account access updated"
},
"unassignedItemsBanner": {
"message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
} }
} }

View File

@ -26,7 +26,7 @@
<bit-hint>{{ "domainNameInputHint" | i18n }}</bit-hint> <bit-hint>{{ "domainNameInputHint" | i18n }}</bit-hint>
</bit-form-field> </bit-form-field>
<bit-form-field> <bit-form-field *ngIf="data?.orgDomain">
<bit-label>{{ "dnsTxtRecord" | i18n }}</bit-label> <bit-label>{{ "dnsTxtRecord" | i18n }}</bit-label>
<input bitInput formControlName="txt" /> <input bitInput formControlName="txt" />
<bit-hint>{{ "dnsTxtRecordInputHint" | i18n }}</bit-hint> <bit-hint>{{ "dnsTxtRecordInputHint" | i18n }}</bit-hint>
@ -42,7 +42,7 @@
</bit-form-field> </bit-form-field>
<bit-callout <bit-callout
*ngIf="!data?.orgDomain?.verifiedDate" *ngIf="data?.orgDomain && !data?.orgDomain?.verifiedDate"
type="info" type="info"
title="{{ 'automaticDomainVerification' | i18n }}" title="{{ 'automaticDomainVerification' | i18n }}"
> >
@ -51,7 +51,10 @@
</div> </div>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary"> <button type="submit" bitButton bitFormButton buttonType="primary">
<span *ngIf="!data?.orgDomain?.verifiedDate">{{ "verifyDomain" | i18n }}</span> <span *ngIf="!data?.orgDomain">{{ "next" | i18n }}</span>
<span *ngIf="data?.orgDomain && !data?.orgDomain?.verifiedDate">{{
"verifyDomain" | i18n
}}</span>
<span *ngIf="data?.orgDomain?.verifiedDate">{{ "reverifyDomain" | i18n }}</span> <span *ngIf="data?.orgDomain?.verifiedDate">{{ "reverifyDomain" | i18n }}</span>
</button> </button>
<button bitButton buttonType="secondary" (click)="dialogRef.close()" type="button"> <button bitButton buttonType="secondary" (click)="dialogRef.close()" type="button">

View File

@ -13,7 +13,6 @@ import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitw
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { domainNameValidator } from "./validators/domain-name.validator"; import { domainNameValidator } from "./validators/domain-name.validator";
@ -90,17 +89,6 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
// Edit // Edit
this.domainForm.patchValue(this.data.orgDomain); this.domainForm.patchValue(this.data.orgDomain);
this.domainForm.disable(); this.domainForm.disable();
} else {
// Add
// Figuring out the proper length of our DNS TXT Record value was fun.
// DNS-Based Service Discovery RFC: https://www.ietf.org/rfc/rfc6763.txt; see section 6.1
// Google uses 43 chars for their TXT record value: https://support.google.com/a/answer/2716802
// So, chose a magic # of 33 bytes to achieve at least that once converted to base 64 (47 char length).
const generatedTxt = `bw=${Utils.fromBufferToB64(
await this.cryptoFunctionService.randomBytes(33),
)}`;
this.txtCtrl.setValue(generatedTxt);
} }
this.setupFormListeners(); this.setupFormListeners();
@ -121,6 +109,7 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
// End Form methods // End Form methods
// Async Form Actions // Async Form Actions
// Creates a new domain record. The DNS TXT Record will be generated server-side and returned in the response.
saveDomain = async (): Promise<void> => { saveDomain = async (): Promise<void> => {
if (this.domainForm.invalid) { if (this.domainForm.invalid) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("domainFormInvalid")); this.platformUtilsService.showToast("error", null, this.i18nService.t("domainFormInvalid"));
@ -130,14 +119,14 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
this.domainNameCtrl.disable(); this.domainNameCtrl.disable();
const request: OrganizationDomainRequest = new OrganizationDomainRequest( const request: OrganizationDomainRequest = new OrganizationDomainRequest(
this.txtCtrl.value,
this.domainNameCtrl.value, this.domainNameCtrl.value,
); );
try { try {
this.data.orgDomain = await this.orgDomainApiService.post(this.data.organizationId, request); this.data.orgDomain = await this.orgDomainApiService.post(this.data.organizationId, request);
// Patch the DNS TXT Record that was generated server-side
this.domainForm.controls.txt.patchValue(this.data.orgDomain.txt);
this.platformUtilsService.showToast("success", null, this.i18nService.t("domainSaved")); this.platformUtilsService.showToast("success", null, this.i18nService.t("domainSaved"));
await this.verifyDomain();
} catch (e) { } catch (e) {
this.handleDomainSaveError(e); this.handleDomainSaveError(e);
} }

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
import { first } from "rxjs/operators"; import { first, switchMap, takeUntil } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
@ -39,7 +39,6 @@ const DisallowedPlanTypes = [
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ClientsComponent implements OnInit { export class ClientsComponent implements OnInit {
providerId: string; providerId: string;
searchText: string;
addableOrganizations: Organization[]; addableOrganizations: Organization[];
loading = true; loading = true;
manageOrganizations = false; manageOrganizations = false;
@ -57,6 +56,17 @@ export class ClientsComponent implements OnInit {
FeatureFlag.EnableConsolidatedBilling, FeatureFlag.EnableConsolidatedBilling,
false, false,
); );
private destroy$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>("");
private isSearching: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -77,27 +87,41 @@ export class ClientsComponent implements OnInit {
) {} ) {}
async ngOnInit() { async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
if (enableConsolidatedBilling) { if (enableConsolidatedBilling) {
await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route }); await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route });
} else { } else {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.params
this.route.parent.params.subscribe(async (params) => { .pipe(
switchMap((params) => {
this.providerId = params.providerId; this.providerId = params.providerId;
return from(this.load());
}),
takeUntil(this.destroy$),
)
.subscribe();
await this.load(); this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => {
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search; this.searchText = qParams.search;
}); });
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearching = isSearchable;
}); });
} }
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
async load() { async load() {
const response = await this.apiService.getProviderClients(this.providerId); const response = await this.apiService.getProviderClients(this.providerId);
this.clients = response.data != null && response.data.length > 0 ? response.data : []; this.clients = response.data != null && response.data.length > 0 ? response.data : [];
@ -118,20 +142,14 @@ export class ClientsComponent implements OnInit {
} }
isPaging() { isPaging() {
const searching = this.isSearching(); const searching = this.isSearching;
if (searching && this.didScroll) { if (searching && this.didScroll) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.resetPaging(); this.resetPaging();
} }
return !searching && this.clients && this.clients.length > this.pageSize; return !searching && this.clients && this.clients.length > this.pageSize;
} }
isSearching() { resetPaging() {
return this.searchService.isSearchable(this.searchText);
}
async resetPaging() {
this.pagedClients = []; this.pagedClients = [];
this.loadMore(); this.loadMore();
} }

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
@ -34,10 +34,7 @@ import { UserAddEditComponent } from "./user-add-edit.component";
templateUrl: "people.component.html", templateUrl: "people.component.html",
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PeopleComponent export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> {
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
implements OnInit
{
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; @ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef; groupsModalRef: ViewContainerRef;
@ -119,6 +116,10 @@ export class PeopleComponent
}); });
} }
ngOnDestroy(): void {
super.ngOnDestroy();
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> { getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
return this.apiService.getProviderUsers(this.providerId); return this.apiService.getProviderUsers(this.providerId);
} }

View File

@ -4,7 +4,7 @@ import { FormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SearchModule } from "@bitwarden/components"; import { SearchModule } from "@bitwarden/components";
import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vault/app/billing";
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { OssModule } from "@bitwarden/web-vault/app/oss.module";
@ -39,6 +39,7 @@ import { SetupComponent } from "./setup/setup.component";
SearchModule, SearchModule,
ProvidersLayoutComponent, ProvidersLayoutComponent,
PaymentMethodWarningsModule, PaymentMethodWarningsModule,
TaxInfoComponent,
], ],
declarations: [ declarations: [
AcceptProviderComponent, AcceptProviderComponent,

View File

@ -25,6 +25,9 @@
required required
/> />
</div> </div>
<div *ngIf="enableConsolidatedBilling$ | async" class="form-group col-12">
<app-tax-info />
</div>
</div> </div>
<div class="mt-4"> <div class="mt-4">

View File

@ -1,9 +1,11 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -12,6 +14,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { ProviderKey } from "@bitwarden/common/types/key"; import { ProviderKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { TaxInfoComponent } from "@bitwarden/web-vault/app/billing";
@Component({ @Component({
selector: "provider-setup", selector: "provider-setup",
@ -19,6 +22,8 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SetupComponent implements OnInit { export class SetupComponent implements OnInit {
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
loading = true; loading = true;
authed = false; authed = false;
email: string; email: string;
@ -34,6 +39,11 @@ export class SetupComponent implements OnInit {
false, false,
); );
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableConsolidatedBilling,
false,
);
constructor( constructor(
private router: Router, private router: Router,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@ -102,6 +112,22 @@ export class SetupComponent implements OnInit {
request.token = this.token; request.token = this.token;
request.key = key; request.key = key;
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
if (enableConsolidatedBilling) {
request.taxInfo = new ExpandedTaxInfoUpdateRequest();
const taxInfoView = this.taxInfoComponent.taxInfo;
request.taxInfo.country = taxInfoView.country;
request.taxInfo.postalCode = taxInfoView.postalCode;
if (taxInfoView.includeTaxId) {
request.taxInfo.taxId = taxInfoView.taxId;
request.taxInfo.line1 = taxInfoView.line1;
request.taxInfo.line2 = taxInfoView.line2;
request.taxInfo.city = taxInfoView.city;
request.taxInfo.state = taxInfoView.state;
}
}
const provider = await this.apiService.postProviderSetup(this.providerId, request); const provider = await this.apiService.postProviderSetup(this.providerId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup")); this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
await this.syncService.fullSync(true); await this.syncService.fullSync(true);

View File

@ -1,8 +1,8 @@
import { SelectionModel } from "@angular/cdk/collections"; import { SelectionModel } from "@angular/cdk/collections";
import { Component, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
import { first } from "rxjs/operators"; import { first, switchMap, takeUntil } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
@ -23,12 +23,22 @@ import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-o
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ManageClientOrganizationsComponent implements OnInit { export class ManageClientOrganizationsComponent implements OnInit, OnDestroy {
providerId: string; providerId: string;
loading = true; loading = true;
manageOrganizations = false; manageOrganizations = false;
private destroy$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>("");
private isSearching: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(search: string) { set searchText(search: string) {
this._searchText$.value;
this.selection.clear(); this.selection.clear();
this.dataSource.filter = search; this.dataSource.filter = search;
} }
@ -67,6 +77,20 @@ export class ManageClientOrganizationsComponent implements OnInit {
this.searchText = qParams.search; this.searchText = qParams.search;
}); });
}); });
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearching = isSearchable;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
async load() { async load() {
@ -80,7 +104,7 @@ export class ManageClientOrganizationsComponent implements OnInit {
} }
isPaging() { isPaging() {
const searching = this.isSearching(); const searching = this.isSearching;
if (searching && this.didScroll) { if (searching && this.didScroll) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
@ -89,10 +113,6 @@ export class ManageClientOrganizationsComponent implements OnInit {
return !searching && this.clients && this.clients.length > this.pageSize; return !searching && this.clients && this.clients.length > this.pageSize;
} }
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
async resetPaging() { async resetPaging() {
this.pagedClients = []; this.pagedClients = [];
this.loadMore(); this.loadMore();

View File

@ -3,16 +3,21 @@ import { Observable, map } from "rxjs";
import { import {
ActiveUserState, ActiveUserState,
KeyDefinition,
SM_ONBOARDING_DISK, SM_ONBOARDING_DISK,
StateProvider, StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state"; } from "@bitwarden/common/platform/state";
export type SMOnboardingTasks = Record<string, Record<string, boolean>>; export type SMOnboardingTasks = Record<string, Record<string, boolean>>;
const SM_ONBOARDING_TASKS_KEY = new KeyDefinition<SMOnboardingTasks>(SM_ONBOARDING_DISK, "tasks", { const SM_ONBOARDING_TASKS_KEY = new UserKeyDefinition<SMOnboardingTasks>(
SM_ONBOARDING_DISK,
"tasks",
{
deserializer: (b) => b, deserializer: (b) => b,
}); clearOn: [], // Used to track tasks completed by a user, we don't want to reshow if they've locked or logged out and came back to the app
},
);
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",

View File

@ -10,7 +10,11 @@ module.exports = {
displayName: "libs/angular tests", displayName: "libs/angular tests",
preset: "jest-preset-angular", preset: "jest-preset-angular",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"], setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { moduleNameMapper: pathsToModuleNameMapper(
// lets us use @bitwarden/common/spec in tests
{ "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) },
{
prefix: "<rootDir>/", prefix: "<rootDir>/",
}), },
),
}; };

View File

@ -12,6 +12,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
@ -56,6 +57,7 @@ export class LockComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor( constructor(
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected router: Router, protected router: Router,
protected i18nService: I18nService, protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService, protected platformUtilsService: PlatformUtilsService,
@ -206,6 +208,7 @@ export class LockComponent implements OnInit, OnDestroy {
} }
private async doUnlockWithMasterPassword() { private async doUnlockWithMasterPassword() {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const kdf = await this.stateService.getKdfType(); const kdf = await this.stateService.getKdfType();
const kdfConfig = await this.stateService.getKdfConfig(); const kdfConfig = await this.stateService.getKdfConfig();
@ -215,11 +218,13 @@ export class LockComponent implements OnInit, OnDestroy {
kdf, kdf,
kdfConfig, kdfConfig,
); );
const storedPasswordHash = await this.cryptoService.getMasterKeyHash(); const storedMasterKeyHash = await firstValueFrom(
this.masterPasswordService.masterKeyHash$(userId),
);
let passwordValid = false; let passwordValid = false;
if (storedPasswordHash != null) { if (storedMasterKeyHash != null) {
// Offline unlock possible // Offline unlock possible
passwordValid = await this.cryptoService.compareAndUpdateKeyHash( passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
this.masterPassword, this.masterPassword,
@ -244,7 +249,7 @@ export class LockComponent implements OnInit, OnDestroy {
masterKey, masterKey,
HashPurpose.LocalAuthorization, HashPurpose.LocalAuthorization,
); );
await this.cryptoService.setMasterKeyHash(localKeyHash); await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} finally { } finally {
@ -262,7 +267,7 @@ export class LockComponent implements OnInit, OnDestroy {
} }
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setMasterKey(masterKey); await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.setUserKeyAndContinue(userKey, true); await this.setUserKeyAndContinue(userKey, true);
} }
@ -292,8 +297,10 @@ export class LockComponent implements OnInit, OnDestroy {
} }
if (this.requirePasswordChange()) { if (this.requirePasswordChange()) {
await this.stateService.setForceSetPasswordReason( const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.WeakMasterPassword, ForceSetPasswordReason.WeakMasterPassword,
userId,
); );
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -12,6 +12,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response"; import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
@ -29,6 +31,7 @@ import {
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@ -45,11 +48,14 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
resetPasswordAutoEnroll = false; resetPasswordAutoEnroll = false;
onSuccessfulChangePassword: () => Promise<void>; onSuccessfulChangePassword: () => Promise<void>;
successRoute = "vault"; successRoute = "vault";
userId: UserId;
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None; forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
ForceSetPasswordReason = ForceSetPasswordReason; ForceSetPasswordReason = ForceSetPasswordReason;
constructor( constructor(
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
i18nService: I18nService, i18nService: I18nService,
cryptoService: CryptoService, cryptoService: CryptoService,
messagingService: MessagingService, messagingService: MessagingService,
@ -88,7 +94,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
await this.syncService.fullSync(true); await this.syncService.fullSync(true);
this.syncLoading = false; this.syncLoading = false;
this.forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
this.forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(this.userId),
);
this.route.queryParams this.route.queryParams
.pipe( .pipe(
@ -176,7 +186,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
if (response == null) { if (response == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
} }
const userId = await this.stateService.getUserId();
const publicKey = Utils.fromB64ToArray(response.publicKey); const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user key with organization public key // RSA Encrypt user key with organization public key
@ -189,7 +198,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
return this.organizationUserService.putOrganizationUserResetPasswordEnrollment( return this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
this.orgId, this.orgId,
userId, this.userId,
resetRequest, resetRequest,
); );
}); });
@ -226,7 +235,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
keyPair: [string, EncString] | null, keyPair: [string, EncString] | null,
) { ) {
// Clear force set password reason to allow navigation back to vault. // Clear force set password reason to allow navigation back to vault.
await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.None,
this.userId,
);
// User now has a password so update account decryption options in state // User now has a password so update account decryption options in state
const userDecryptionOpts = await firstValueFrom( const userDecryptionOpts = await firstValueFrom(
@ -237,7 +249,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
await this.stateService.setKdfType(this.kdf); await this.stateService.setKdfType(this.kdf);
await this.stateService.setKdfConfig(this.kdfConfig); await this.stateService.setKdfConfig(this.kdfConfig);
await this.cryptoService.setMasterKey(masterKey); await this.masterPasswordService.setMasterKey(masterKey, this.userId);
await this.cryptoService.setUserKey(userKey[0]); await this.cryptoService.setUserKey(userKey[0]);
// Set private key only for new JIT provisioned users in MP encryption orgs // Set private key only for new JIT provisioned users in MP encryption orgs
@ -255,6 +267,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
masterKey, masterKey,
HashPurpose.LocalAuthorization, HashPurpose.LocalAuthorization,
); );
await this.cryptoService.setMasterKeyHash(localMasterKeyHash); await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId);
} }
} }

View File

@ -12,10 +12,13 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -23,7 +26,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { UserId } from "@bitwarden/common/types/guid";
import { SsoComponent } from "./sso.component"; import { SsoComponent } from "./sso.component";
// test component that extends the SsoComponent // test component that extends the SsoComponent
@ -48,6 +53,7 @@ describe("SsoComponent", () => {
let component: TestSsoComponent; let component: TestSsoComponent;
let _component: SsoComponentProtected; let _component: SsoComponentProtected;
let fixture: ComponentFixture<TestSsoComponent>; let fixture: ComponentFixture<TestSsoComponent>;
const userId = "userId" as UserId;
// Mock Services // Mock Services
let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
@ -67,6 +73,8 @@ describe("SsoComponent", () => {
let mockLogService: MockProxy<LogService>; let mockLogService: MockProxy<LogService>;
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>; let mockConfigService: MockProxy<ConfigService>;
let mockMasterPasswordService: FakeMasterPasswordService;
let mockAccountService: FakeAccountService;
// Mock authService.logIn params // Mock authService.logIn params
let code: string; let code: string;
@ -117,6 +125,8 @@ describe("SsoComponent", () => {
mockLogService = mock(); mockLogService = mock();
mockUserDecryptionOptionsService = mock(); mockUserDecryptionOptionsService = mock();
mockConfigService = mock(); mockConfigService = mock();
mockAccountService = mockAccountServiceWith(userId);
mockMasterPasswordService = new FakeMasterPasswordService();
// Mock loginStrategyService.logIn params // Mock loginStrategyService.logIn params
code = "code"; code = "code";
@ -199,6 +209,8 @@ describe("SsoComponent", () => {
}, },
{ provide: LogService, useValue: mockLogService }, { provide: LogService, useValue: mockLogService },
{ provide: ConfigService, useValue: mockConfigService }, { provide: ConfigService, useValue: mockConfigService },
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
{ provide: AccountService, useValue: mockAccountService },
], ],
}); });
@ -365,8 +377,9 @@ describe("SsoComponent", () => {
await _component.logIn(code, codeVerifier, orgIdFromState); await _component.logIn(code, codeVerifier, orgIdFromState);
expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1); expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1);
expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith( expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
userId,
); );
expect(mockOnSuccessfulLoginTdeNavigate).not.toHaveBeenCalled(); expect(mockOnSuccessfulLoginTdeNavigate).not.toHaveBeenCalled();

View File

@ -11,6 +11,8 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@ -66,6 +68,8 @@ export class SsoComponent {
protected logService: LogService, protected logService: LogService,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected configService: ConfigService, protected configService: ConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected accountService: AccountService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -290,8 +294,10 @@ export class SsoComponent {
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
// Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and // Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and
// if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key.
await this.stateService.setForceSetPasswordReason( const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
userId,
); );
} }

View File

@ -15,11 +15,14 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -27,6 +30,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { TwoFactorComponent } from "./two-factor.component"; import { TwoFactorComponent } from "./two-factor.component";
@ -46,6 +51,7 @@ describe("TwoFactorComponent", () => {
let _component: TwoFactorComponentProtected; let _component: TwoFactorComponentProtected;
let fixture: ComponentFixture<TestTwoFactorComponent>; let fixture: ComponentFixture<TestTwoFactorComponent>;
const userId = "userId" as UserId;
// Mock Services // Mock Services
let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>; let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
@ -63,6 +69,8 @@ describe("TwoFactorComponent", () => {
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>; let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>; let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
let mockConfigService: MockProxy<ConfigService>; let mockConfigService: MockProxy<ConfigService>;
let mockMasterPasswordService: FakeMasterPasswordService;
let mockAccountService: FakeAccountService;
let mockUserDecryptionOpts: { let mockUserDecryptionOpts: {
noMasterPassword: UserDecryptionOptions; noMasterPassword: UserDecryptionOptions;
@ -93,6 +101,8 @@ describe("TwoFactorComponent", () => {
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>(); mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
mockSsoLoginService = mock<SsoLoginServiceAbstraction>(); mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
mockConfigService = mock<ConfigService>(); mockConfigService = mock<ConfigService>();
mockAccountService = mockAccountServiceWith(userId);
mockMasterPasswordService = new FakeMasterPasswordService();
mockUserDecryptionOpts = { mockUserDecryptionOpts = {
noMasterPassword: new UserDecryptionOptions({ noMasterPassword: new UserDecryptionOptions({
@ -170,6 +180,8 @@ describe("TwoFactorComponent", () => {
}, },
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
{ provide: ConfigService, useValue: mockConfigService }, { provide: ConfigService, useValue: mockConfigService },
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
{ provide: AccountService, useValue: mockAccountService },
], ],
}); });
@ -407,9 +419,9 @@ describe("TwoFactorComponent", () => {
await component.doSubmit(); await component.doSubmit();
// Assert // Assert
expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith(
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
userId,
); );
expect(mockRouter.navigate).toHaveBeenCalledTimes(1); expect(mockRouter.navigate).toHaveBeenCalledTimes(1);

View File

@ -14,6 +14,8 @@ import {
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
@ -92,6 +94,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected ssoLoginService: SsoLoginServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction,
protected configService: ConfigService, protected configService: ConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected accountService: AccountService,
) { ) {
super(environmentService, i18nService, platformUtilsService); super(environmentService, i18nService, platformUtilsService);
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
@ -342,8 +346,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
// Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and // Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and
// if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key.
await this.stateService.setForceSetPasswordReason( const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
userId,
); );
} }

View File

@ -1,9 +1,12 @@
import { Directive } from "@angular/core"; import { Directive } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@ -56,6 +59,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
protected router: Router, protected router: Router,
dialogService: DialogService, dialogService: DialogService,
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
i18nService, i18nService,
@ -72,7 +77,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
async ngOnInit() { async ngOnInit() {
await this.syncService.fullSync(true); await this.syncService.fullSync(true);
this.reason = await this.stateService.getForceSetPasswordReason(); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId));
// If we somehow end up here without a reason, go back to the home page // If we somehow end up here without a reason, go back to the home page
if (this.reason == ForceSetPasswordReason.None) { if (this.reason == ForceSetPasswordReason.None) {
@ -163,7 +169,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
this.i18nService.t("updatedMasterPassword"), this.i18nService.t("updatedMasterPassword"),
); );
await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.masterPasswordService.setForceSetPasswordReason(
ForceSetPasswordReason.None,
userId,
);
if (this.onSuccessfulChangePassword != null) { if (this.onSuccessfulChangePassword != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@ -1,12 +1,14 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@Injectable() @Injectable()
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
@ -15,7 +17,8 @@ export class AuthGuard implements CanActivate {
private router: Router, private router: Router,
private messagingService: MessagingService, private messagingService: MessagingService,
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService,
private stateService: StateService, private accountService: AccountService,
private masterPasswordService: MasterPasswordServiceAbstraction,
) {} ) {}
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
@ -40,7 +43,10 @@ export class AuthGuard implements CanActivate {
return this.router.createUrlTree(["/remove-password"]); return this.router.createUrlTree(["/remove-password"]);
} }
const forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
const forceSetPasswordReason = await firstValueFrom(
this.masterPasswordService.forceSetPasswordReason$(userId),
);
if ( if (
forceSetPasswordReason === forceSetPasswordReason ===

View File

@ -60,6 +60,10 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
import {
InternalMasterPasswordServiceAbstraction,
MasterPasswordServiceAbstraction,
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
@ -78,6 +82,7 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation"; import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service";
@ -359,6 +364,8 @@ const safeProviders: SafeProvider[] = [
provide: LoginStrategyServiceAbstraction, provide: LoginStrategyServiceAbstraction,
useClass: LoginStrategyService, useClass: LoginStrategyService,
deps: [ deps: [
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
TokenServiceAbstraction, TokenServiceAbstraction,
@ -521,6 +528,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction, provide: CryptoServiceAbstraction,
useClass: CryptoService, useClass: CryptoService,
deps: [ deps: [
InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction, KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
EncryptService, EncryptService,
@ -587,6 +595,8 @@ const safeProviders: SafeProvider[] = [
provide: SyncServiceAbstraction, provide: SyncServiceAbstraction,
useClass: SyncService, useClass: SyncService,
deps: [ deps: [
InternalMasterPasswordServiceAbstraction,
AccountServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
DomainSettingsService, DomainSettingsService,
InternalFolderService, InternalFolderService,
@ -626,6 +636,8 @@ const safeProviders: SafeProvider[] = [
provide: VaultTimeoutService, provide: VaultTimeoutService,
useClass: VaultTimeoutService, useClass: VaultTimeoutService,
deps: [ deps: [
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
CipherServiceAbstraction, CipherServiceAbstraction,
FolderServiceAbstraction, FolderServiceAbstraction,
CollectionServiceAbstraction, CollectionServiceAbstraction,
@ -714,7 +726,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: SearchServiceAbstraction, provide: SearchServiceAbstraction,
useClass: SearchService, useClass: SearchService,
deps: [LogService, I18nServiceAbstraction], deps: [LogService, I18nServiceAbstraction, StateProvider],
}), }),
safeProvider({ safeProvider({
provide: NotificationsServiceAbstraction, provide: NotificationsServiceAbstraction,
@ -771,10 +783,21 @@ const safeProviders: SafeProvider[] = [
useClass: PolicyApiService, useClass: PolicyApiService,
deps: [InternalPolicyService, ApiServiceAbstraction], deps: [InternalPolicyService, ApiServiceAbstraction],
}), }),
safeProvider({
provide: InternalMasterPasswordServiceAbstraction,
useClass: MasterPasswordService,
deps: [StateProvider],
}),
safeProvider({
provide: MasterPasswordServiceAbstraction,
useExisting: InternalMasterPasswordServiceAbstraction,
}),
safeProvider({ safeProvider({
provide: KeyConnectorServiceAbstraction, provide: KeyConnectorServiceAbstraction,
useClass: KeyConnectorService, useClass: KeyConnectorService,
deps: [ deps: [
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
TokenServiceAbstraction, TokenServiceAbstraction,
@ -791,6 +814,8 @@ const safeProviders: SafeProvider[] = [
deps: [ deps: [
StateServiceAbstraction, StateServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
I18nServiceAbstraction, I18nServiceAbstraction,
UserVerificationApiServiceAbstraction, UserVerificationApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
@ -934,9 +959,10 @@ const safeProviders: SafeProvider[] = [
useClass: AuthRequestService, useClass: AuthRequestService,
deps: [ deps: [
AppIdServiceAbstraction, AppIdServiceAbstraction,
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
StateServiceAbstraction,
], ],
}), }),
safeProvider({ safeProvider({

View File

@ -0,0 +1,19 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@Injectable({ providedIn: "root" })
export class UnassignedItemsBannerApiService {
constructor(private apiService: ApiService) {}
async getShowUnassignedCiphersBanner(): Promise<boolean> {
const r = await this.apiService.send(
"GET",
"/ciphers/has-unassigned-ciphers",
null,
true,
true,
);
return r;
}
}

View File

@ -0,0 +1,51 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service";
import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service";
describe("UnassignedItemsBanner", () => {
let stateProvider: FakeStateProvider;
let apiService: MockProxy<UnassignedItemsBannerApiService>;
const sutFactory = () => new UnassignedItemsBannerService(stateProvider, apiService);
beforeEach(() => {
const fakeAccountService = mockAccountServiceWith("userId" as UserId);
stateProvider = new FakeStateProvider(fakeAccountService);
apiService = mock();
});
it("shows the banner if showBanner local state is true", async () => {
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
showBanner.nextState(true);
const sut = sutFactory();
expect(await firstValueFrom(sut.showBanner$)).toBe(true);
expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled();
});
it("does not show the banner if showBanner local state is false", async () => {
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
showBanner.nextState(false);
const sut = sutFactory();
expect(await firstValueFrom(sut.showBanner$)).toBe(false);
expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled();
});
it("fetches from server if local state has not been set yet", async () => {
apiService.getShowUnassignedCiphersBanner.mockResolvedValue(true);
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
showBanner.nextState(undefined);
const sut = sutFactory();
expect(await firstValueFrom(sut.showBanner$)).toBe(true);
expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,47 @@
import { Injectable } from "@angular/core";
import { concatMap } from "rxjs";
import {
StateProvider,
UNASSIGNED_ITEMS_BANNER_DISK,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service";
export const SHOW_BANNER_KEY = new UserKeyDefinition<boolean>(
UNASSIGNED_ITEMS_BANNER_DISK,
"showBanner",
{
deserializer: (b) => b,
clearOn: [],
},
);
/** Displays a banner that tells users how to move their unassigned items into a collection. */
@Injectable({ providedIn: "root" })
export class UnassignedItemsBannerService {
private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY);
showBanner$ = this._showBanner.state$.pipe(
concatMap(async (showBannerState) => {
// null indicates that the user has not seen or dismissed the banner yet - get the flag from server
if (showBannerState == null) {
const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner();
await this._showBanner.update(() => showBannerResponse);
return showBannerResponse;
}
return showBannerState;
}),
);
constructor(
private stateProvider: StateProvider,
private apiService: UnassignedItemsBannerApiService,
) {}
async hideBanner() {
await this._showBanner.update(() => false);
}
}

View File

@ -1,5 +1,13 @@
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Subject, firstValueFrom, mergeMap, takeUntil } from "rxjs"; import {
BehaviorSubject,
Subject,
firstValueFrom,
mergeMap,
from,
switchMap,
takeUntil,
} from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -24,7 +32,6 @@ export class SendComponent implements OnInit, OnDestroy {
expired = false; expired = false;
type: SendType = null; type: SendType = null;
sends: SendView[] = []; sends: SendView[] = [];
searchText: string;
selectedType: SendType; selectedType: SendType;
selectedAll: boolean; selectedAll: boolean;
filter: (cipher: SendView) => boolean; filter: (cipher: SendView) => boolean;
@ -39,6 +46,8 @@ export class SendComponent implements OnInit, OnDestroy {
private searchTimeout: any; private searchTimeout: any;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private _filteredSends: SendView[]; private _filteredSends: SendView[];
private _searchText$ = new BehaviorSubject<string>("");
protected isSearchable: boolean = false;
get filteredSends(): SendView[] { get filteredSends(): SendView[] {
return this._filteredSends; return this._filteredSends;
@ -48,6 +57,14 @@ export class SendComponent implements OnInit, OnDestroy {
this._filteredSends = filteredSends; this._filteredSends = filteredSends;
} }
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor( constructor(
protected sendService: SendService, protected sendService: SendService,
protected i18nService: I18nService, protected i18nService: I18nService,
@ -68,6 +85,15 @@ export class SendComponent implements OnInit, OnDestroy {
.subscribe((policyAppliesToActiveUser) => { .subscribe((policyAppliesToActiveUser) => {
this.disableSend = policyAppliesToActiveUser; this.disableSend = policyAppliesToActiveUser;
}); });
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -122,14 +148,14 @@ export class SendComponent implements OnInit, OnDestroy {
clearTimeout(this.searchTimeout); clearTimeout(this.searchTimeout);
} }
if (timeout == null) { if (timeout == null) {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = this.isSearchable;
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
this.applyTextSearch(); this.applyTextSearch();
return; return;
} }
this.searchPending = true; this.searchPending = true;
this.searchTimeout = setTimeout(async () => { this.searchTimeout = setTimeout(async () => {
this.hasSearched = this.searchService.isSearchable(this.searchText); this.hasSearched = this.isSearchable;
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
this.applyTextSearch(); this.applyTextSearch();
this.searchPending = false; this.searchPending = false;

View File

@ -1,4 +1,5 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@ -6,7 +7,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Directive() @Directive()
export class VaultItemsComponent { export class VaultItemsComponent implements OnInit, OnDestroy {
@Input() activeCipherId: string = null; @Input() activeCipherId: string = null;
@Output() onCipherClicked = new EventEmitter<CipherView>(); @Output() onCipherClicked = new EventEmitter<CipherView>();
@Output() onCipherRightClicked = new EventEmitter<CipherView>(); @Output() onCipherRightClicked = new EventEmitter<CipherView>();
@ -23,13 +24,15 @@ export class VaultItemsComponent {
protected searchPending = false; protected searchPending = false;
private destroy$ = new Subject<void>();
private searchTimeout: any = null; private searchTimeout: any = null;
private _searchText: string = null; private isSearchable: boolean = false;
private _searchText$ = new BehaviorSubject<string>("");
get searchText() { get searchText() {
return this._searchText; return this._searchText$.value;
} }
set searchText(value: string) { set searchText(value: string) {
this._searchText = value; this._searchText$.next(value);
} }
constructor( constructor(
@ -37,6 +40,21 @@ export class VaultItemsComponent {
protected cipherService: CipherService, protected cipherService: CipherService,
) {} ) {}
ngOnInit(): void {
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
}
ngOnDestroy(): void {
throw new Error("Method not implemented.");
}
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) { async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
this.deleted = deleted ?? false; this.deleted = deleted ?? false;
await this.applyFilter(filter); await this.applyFilter(filter);
@ -90,7 +108,7 @@ export class VaultItemsComponent {
} }
isSearching() { isSearching() {
return !this.searchPending && this.searchService.isSearchable(this.searchText); return !this.searchPending && this.isSearchable;
} }
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;

View File

@ -5,6 +5,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -14,7 +15,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { CsprngArray } from "@bitwarden/common/types/csprng"; import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
@ -42,6 +45,10 @@ describe("AuthRequestLoginStrategy", () => {
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>; let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>; let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let masterPasswordService: FakeMasterPasswordService;
let authRequestLoginStrategy: AuthRequestLoginStrategy; let authRequestLoginStrategy: AuthRequestLoginStrategy;
let credentials: AuthRequestLoginCredentials; let credentials: AuthRequestLoginCredentials;
let tokenResponse: IdentityTokenResponse; let tokenResponse: IdentityTokenResponse;
@ -71,12 +78,17 @@ describe("AuthRequestLoginStrategy", () => {
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>(); deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>(); billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
accountService = mockAccountServiceWith(mockUserId);
masterPasswordService = new FakeMasterPasswordService();
tokenService.getTwoFactorToken.mockResolvedValue(null); tokenService.getTwoFactorToken.mockResolvedValue(null);
appIdService.getAppId.mockResolvedValue(deviceId); appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeAccessToken.mockResolvedValue({}); tokenService.decodeAccessToken.mockResolvedValue({});
authRequestLoginStrategy = new AuthRequestLoginStrategy( authRequestLoginStrategy = new AuthRequestLoginStrategy(
cache, cache,
accountService,
masterPasswordService,
cryptoService, cryptoService,
apiService, apiService,
tokenService, tokenService,
@ -108,13 +120,16 @@ describe("AuthRequestLoginStrategy", () => {
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
cryptoService.getMasterKey.mockResolvedValue(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await authRequestLoginStrategy.logIn(credentials); await authRequestLoginStrategy.logIn(credentials);
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, mockUserId);
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash); expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith(
decMasterKeyHash,
mockUserId,
);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled();
@ -136,8 +151,8 @@ describe("AuthRequestLoginStrategy", () => {
await authRequestLoginStrategy.logIn(credentials); await authRequestLoginStrategy.logIn(credentials);
// setMasterKey and setMasterKeyHash should not be called // setMasterKey and setMasterKeyHash should not be called
expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();
expect(cryptoService.setMasterKeyHash).not.toHaveBeenCalled(); expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled();
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);

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