Merge branch 'main' into AC-2413-Migrate-policies-component

This commit is contained in:
vinith-kovan 2024-05-09 10:49:01 +05:30 committed by GitHub
commit ee795ef8d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
357 changed files with 5200 additions and 3704 deletions

View File

@ -4,6 +4,7 @@ import remarkGfm from "remark-gfm";
const config: StorybookConfig = {
stories: [
"../libs/auth/src/**/*.mdx",
"../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)",
"../libs/components/src/**/*.mdx",
"../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)",

View File

@ -142,7 +142,15 @@
"configDir": ".storybook",
"browserTarget": "components:build",
"compodoc": true,
"compodocArgs": ["-p", "./tsconfig.json", "-e", "json", "-d", "."],
"compodocArgs": [
"-p",
"./tsconfig.json",
"-e",
"json",
"-d",
".",
"--disableRoutesGraph"
],
"port": 6006
}
},

View File

@ -1,5 +1,5 @@
{
"dev_flags": {},
"devFlags": {},
"flags": {
"showPasswordless": true,
"enableCipherKeyEncryption": false,

View File

@ -2,7 +2,8 @@
"devFlags": {
"managedEnvironment": {
"base": "https://localhost:8080"
}
},
"skipWelcomeOnInstall": true
},
"flags": {
"showPasswordless": true,

View File

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

View File

@ -374,12 +374,21 @@
"other": {
"message": "Other"
},
"unlockMethods": {
"message": "Unlock options"
},
"unlockMethodNeededToChangeTimeoutActionDesc": {
"message": "Set up an unlock method to change your vault timeout action."
},
"unlockMethodNeeded": {
"message": "Set up an unlock method in Settings"
},
"sessionTimeoutHeader": {
"message": "Session timeout"
},
"otherOptions": {
"message": "Other options"
},
"rateExtension": {
"message": "Rate the extension"
},
@ -3023,6 +3032,15 @@
"adminConsole": {
"message": "Admin Console"
},
"accountSecurity": {
"message": "Account security"
},
"notifications": {
"message": "Notifications"
},
"appearance": {
"message": "Appearance"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
},

View File

@ -226,7 +226,7 @@
"message": "Help & feedback"
},
"helpCenter": {
"message": "Bitwarden Help center"
"message": "Bitwarden Help centre"
},
"communityForums": {
"message": "Explore Bitwarden community forums"
@ -728,7 +728,7 @@
"message": "Change the application's colour theme."
},
"themeDescAlt": {
"message": "Change the application's color theme. Applies to all logged in accounts."
"message": "Change the application's colour theme. Applies to all logged in accounts."
},
"dark": {
"message": "Dark",
@ -1165,7 +1165,7 @@
"message": "Show a recognizable image next to each login."
},
"faviconDescAlt": {
"message": "Show a recognizable image next to each login. Applies to all logged in accounts."
"message": "Show a recognisable image next to each login. Applies to all logged in accounts."
},
"enableBadgeCounter": {
"message": "Show badge counter"
@ -1730,7 +1730,7 @@
"message": "An organization policy is affecting your ownership options."
},
"personalOwnershipPolicyInEffectImports": {
"message": "An organization policy has blocked importing items into your individual vault."
"message": "An organisation policy has blocked importing items into your individual vault."
},
"excludedDomains": {
"message": "Excluded Domains"
@ -1990,7 +1990,7 @@
"message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"updateWeakMasterPasswordWarning": {
"message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
"message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Automatic Enrollment"
@ -2006,11 +2006,11 @@
"description": "Used as a message within the notification bar when no folders are found"
},
"orgPermissionsUpdatedMustSetPassword": {
"message": "Your organization permissions were updated, requiring you to set a master password.",
"message": "Your organisation permissions were updated, requiring you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"orgRequiresYouToSetPassword": {
"message": "Your organization requires you to set a master password.",
"message": "Your organisation requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"verificationRequired": {
@ -2037,7 +2037,7 @@
}
},
"vaultTimeoutPolicyWithActionInEffect": {
"message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
"message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
"placeholders": {
"hours": {
"content": "$1",
@ -2054,7 +2054,7 @@
}
},
"vaultTimeoutActionPolicyInEffect": {
"message": "Your organization policies have set your vault timeout action to $ACTION$.",
"message": "Your organisation policies have set your vault timeout action to $ACTION$.",
"placeholders": {
"action": {
"content": "$1",
@ -2111,7 +2111,7 @@
"message": "Exporting Personal Vault"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"placeholders": {
"email": {
"content": "$1",
@ -2305,7 +2305,7 @@
}
},
"autofillPageLoadPolicyActivated": {
"message": "Your organization policies have turned on auto-fill on page load."
"message": "Your organisation policies have turned on auto-fill on page load."
},
"howToAutofill": {
"message": "How to auto-fill"
@ -2377,7 +2377,7 @@
"message": "Approve with master password"
},
"ssoIdentifierRequired": {
"message": "Organization SSO identifier is required."
"message": "Organisation SSO identifier is required."
},
"eu": {
"message": "EU",
@ -2688,7 +2688,7 @@
"message": "Total"
},
"importWarning": {
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?",
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?",
"placeholders": {
"organization": {
"content": "$1",
@ -3007,10 +3007,10 @@
"message": "Passkey removed"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."
"message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console."
},
"unassignedItemsBannerSelfHostNotice": {
"message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console."
"message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",

View File

@ -3,7 +3,7 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Bitwarden - Administrador de contraseñas",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
@ -2962,27 +2962,27 @@
"description": "Label indicating the most common import formats"
},
"overrideDefaultBrowserAutofillTitle": {
"message": "¿Quiere hacer de Bitwarden su gestor de contraseñas predeterminado?",
"message": "¿Hacer de Bitwarden su administrador de contraseñas predeterminado?",
"description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutofillDescription": {
"message": "Pasar por alto esta opción puede causar conflictos entre el menú de relleno automático de Bitwarden y el del navegador.",
"message": "Pasar por alto esta opción puede causar conflictos entre el menú de autocompletar de Bitwarden y el de tu navegador.",
"description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutoFillSettings": {
"message": "Hacer de Bitwarden su gestor de contraseñas predeterminado",
"message": "Hacer de Bitwarden tu administrador de contraseñas predeterminado",
"description": "Label for the setting that allows overriding the default browser autofill settings"
},
"privacyPermissionAdditionNotGrantedTitle": {
"message": "No se pudo establecer Bitwarden como el gestor de contraseñas predeterminado",
"message": "No se puede establecer Bitwarden como el administrador de contraseñas predeterminado",
"description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings"
},
"privacyPermissionAdditionNotGrantedDescription": {
"message": "Debe otorgar los permisos de privacidad del navegador a Bitwarden para establecerlo como gestor de contraseñas predeterminado.",
"message": "Debes otorgar permisos de privacidad del navegador a Bitwarden para establecerlo como administrador de contraseñas predeterminado.",
"description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings"
},
"makeDefault": {
"message": "Predeterminar",
"message": "Establecer como predeterminado",
"description": "Button text for the setting that allows overriding the default browser autofill settings"
},
"saveCipherAttemptSuccess": {
@ -2998,7 +2998,7 @@
"description": "Notification message for when saving credentials has failed."
},
"success": {
"message": "Success"
"message": "Éxito"
},
"removePasskey": {
"message": "Eliminar passkey"
@ -3013,20 +3013,20 @@
"message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",
"message": "Asignar estos elementos a una colección de",
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
},
"unassignedItemsBannerCTAPartTwo": {
"message": "to make them visible.",
"message": "para hcerlos visibles.",
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
},
"adminConsole": {
"message": "Admin Console"
"message": "Consola de administrador"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
"message": "Error al asignar la colección de destino."
},
"errorAssigningTargetFolder": {
"message": "Error assigning target folder."
"message": "Error al asignar la carpeta de destino."
}
}

View File

@ -173,10 +173,10 @@
"message": "Keisti pagrindinį slaptažodį"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "Tęsti į žiniatinklio programėlę?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "Pagrindinį slaptažodį galite pakeisti „Bitwarden“ žiniatinklio programėlėje."
},
"fingerprintPhrase": {
"message": "Pirštų atspaudų frazė",
@ -2998,7 +2998,7 @@
"description": "Notification message for when saving credentials has failed."
},
"success": {
"message": "Success"
"message": "Sėkmė"
},
"removePasskey": {
"message": "Pašalinti slaptaraktį"
@ -3007,26 +3007,26 @@
"message": "Pašalintas slaptaraktis"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."
"message": "Pranešimas: nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę."
},
"unassignedItemsBannerSelfHostNotice": {
"message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console."
"message": "Pranešimas: 2024 m. gegužės 16 d. nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",
"message": "Priskirkite šiuos elementus kolekcijai iš",
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
},
"unassignedItemsBannerCTAPartTwo": {
"message": "to make them visible.",
"message": ", kad jie būtų matomi.",
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
},
"adminConsole": {
"message": "Admin Console"
"message": "Administratoriaus konsolės"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
"message": "Klaida priskiriant tikslinę kolekciją."
},
"errorAssigningTargetFolder": {
"message": "Error assigning target folder."
"message": "Klaida priskiriant tikslinį aplanką."
}
}

View File

@ -34,6 +34,7 @@ import {
KeyGenerationServiceInitOptions,
keyGenerationServiceFactory,
} from "../../../platform/background/service-factories/key-generation-service.factory";
import { logServiceFactory } from "../../../platform/background/service-factories/log-service.factory";
import {
PlatformUtilsServiceInitOptions,
platformUtilsServiceFactory,
@ -88,6 +89,7 @@ export function deviceTrustServiceFactory(
await stateProviderFactory(cache, opts),
await secureStorageServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
),
);
}

View File

@ -4,20 +4,35 @@ import {
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import {
encryptServiceFactory,
EncryptServiceInitOptions,
} from "../../../platform/background/service-factories/encrypt-service.factory";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
keyGenerationServiceFactory,
KeyGenerationServiceInitOptions,
} from "../../../platform/background/service-factories/key-generation-service.factory";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../../platform/background/service-factories/state-service.factory";
type MasterPasswordServiceFactoryOptions = FactoryOptions;
export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions &
StateProviderInitOptions;
StateProviderInitOptions &
StateServiceInitOptions &
KeyGenerationServiceInitOptions &
EncryptServiceInitOptions;
export function internalMasterPasswordServiceFactory(
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
@ -27,7 +42,13 @@ export function internalMasterPasswordServiceFactory(
cache,
"masterPasswordService",
opts,
async () => new MasterPasswordService(await stateProviderFactory(cache, opts)),
async () =>
new MasterPasswordService(
await stateProviderFactory(cache, opts),
await stateServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
),
);
}

View File

@ -1,53 +0,0 @@
import { PinCryptoServiceAbstraction, PinCryptoService } from "@bitwarden/auth/common";
import {
VaultTimeoutSettingsServiceInitOptions,
vaultTimeoutSettingsServiceFactory,
} from "../../../background/service-factories/vault-timeout-settings-service.factory";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
} from "../../../platform/background/service-factories/crypto-service.factory";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
LogServiceInitOptions,
logServiceFactory,
} from "../../../platform/background/service-factories/log-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory";
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
type PinCryptoServiceFactoryOptions = FactoryOptions;
export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions &
StateServiceInitOptions &
CryptoServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions &
LogServiceInitOptions &
KdfConfigServiceInitOptions;
export function pinCryptoServiceFactory(
cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices,
opts: PinCryptoServiceInitOptions,
): Promise<PinCryptoServiceAbstraction> {
return factory(
cache,
"pinCryptoService",
opts,
async () =>
new PinCryptoService(
await stateServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await kdfConfigServiceFactory(cache, opts),
),
);
}

View File

@ -0,0 +1,74 @@
import { PinServiceAbstraction, PinService } from "@bitwarden/auth/common";
import {
CryptoFunctionServiceInitOptions,
cryptoFunctionServiceFactory,
} from "../../../platform/background/service-factories/crypto-function-service.factory";
import {
EncryptServiceInitOptions,
encryptServiceFactory,
} from "../../../platform/background/service-factories/encrypt-service.factory";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
KeyGenerationServiceInitOptions,
keyGenerationServiceFactory,
} from "../../../platform/background/service-factories/key-generation-service.factory";
import {
LogServiceInitOptions,
logServiceFactory,
} from "../../../platform/background/service-factories/log-service.factory";
import {
StateProviderInitOptions,
stateProviderFactory,
} from "../../../platform/background/service-factories/state-provider.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
} from "../../../platform/background/service-factories/state-service.factory";
import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory";
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
import {
MasterPasswordServiceInitOptions,
masterPasswordServiceFactory,
} from "./master-password-service.factory";
type PinServiceFactoryOptions = FactoryOptions;
export type PinServiceInitOptions = PinServiceFactoryOptions &
AccountServiceInitOptions &
CryptoFunctionServiceInitOptions &
EncryptServiceInitOptions &
KdfConfigServiceInitOptions &
KeyGenerationServiceInitOptions &
LogServiceInitOptions &
MasterPasswordServiceInitOptions &
StateProviderInitOptions &
StateServiceInitOptions;
export function pinServiceFactory(
cache: { pinService?: PinServiceAbstraction } & CachedServices,
opts: PinServiceInitOptions,
): Promise<PinServiceAbstraction> {
return factory(
cache,
"pinService",
opts,
async () =>
new PinService(
await accountServiceFactory(cache, opts),
await cryptoFunctionServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
await kdfConfigServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await masterPasswordServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
await stateServiceFactory(cache, opts),
),
);
}

View File

@ -37,7 +37,7 @@ import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "./master-password-service.factory";
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory";
import { PinServiceInitOptions, pinServiceFactory } from "./pin-service.factory";
import {
userDecryptionOptionsServiceFactory,
UserDecryptionOptionsServiceInitOptions,
@ -57,7 +57,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
I18nServiceInitOptions &
UserVerificationApiServiceInitOptions &
UserDecryptionOptionsServiceInitOptions &
PinCryptoServiceInitOptions &
PinServiceInitOptions &
LogServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions &
PlatformUtilsServiceInitOptions &
@ -80,7 +80,7 @@ export function userVerificationServiceFactory(
await i18nServiceFactory(cache, opts),
await userVerificationApiServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
await pinCryptoServiceFactory(cache, opts),
await pinServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts),

View File

@ -12,8 +12,16 @@
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart">
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" />
<label
class="tw-flex tw-items-start tw-gap-2"
*ngIf="showMasterPasswordOnClientRestartOption"
>
<input
class="tw-mt-1"
type="checkbox"
bitCheckbox
formControlName="requireMasterPasswordOnClientRestart"
/>
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
</label>
</div>

View File

@ -3,7 +3,7 @@ import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction } 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";
@ -63,7 +63,7 @@ export class LockComponent extends BaseLockComponent {
dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService,
pinCryptoService: PinCryptoServiceAbstraction,
pinService: PinServiceAbstraction,
private routerService: BrowserRouterService,
biometricStateService: BiometricStateService,
accountService: AccountService,
@ -89,7 +89,7 @@ export class LockComponent extends BaseLockComponent {
dialogService,
deviceTrustService,
userVerificationService,
pinCryptoService,
pinService,
biometricStateService,
accountService,
authService,
@ -143,15 +143,17 @@ export class LockComponent extends BaseLockComponent {
try {
success = await super.unlockBiometric();
} catch (e) {
const error = BiometricErrors[e as BiometricErrorTypes];
const error = BiometricErrors[e?.message as BiometricErrorTypes];
if (error == null) {
this.logService.error("Unknown error: " + e);
return false;
}
this.biometricError = this.i18nService.t(error.description);
} finally {
this.pendingBiometric = false;
}
this.pendingBiometric = false;
return success;
}

View File

@ -0,0 +1,140 @@
<app-header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "accountSecurity" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</app-header>
<main tabindex="-1" [formGroup]="form">
<div class="box list">
<h2 class="box-header">{{ "unlockMethods" | i18n }}</h2>
<div class="box-content single-line">
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
<input id="biometric" type="checkbox" formControlName="biometric" />
</div>
<div
class="box-content-row box-content-row-checkbox"
appBoxRow
*ngIf="supportsBiometric && this.form.value.biometric"
>
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
<input
id="autoBiometricsPrompt"
type="checkbox"
(change)="updateAutoBiometricsPrompt()"
formControlName="enableAutoBiometricsPrompt"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
<input id="pin" type="checkbox" formControlName="pin" />
</div>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "sessionTimeoutHeader" | i18n }}</h2>
<div class="box-content single-line">
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
<span *ngIf="policy.timeout && policy.action">
{{
"vaultTimeoutPolicyWithActionInEffect"
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
}}
</span>
<span *ngIf="policy.timeout && !policy.action">
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
</span>
<span *ngIf="!policy.timeout && policy.action">
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
</span>
</app-callout>
<app-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</app-vault-timeout-input>
<div class="box-content-row display-block" appBoxRow>
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
<select
id="vaultTimeoutAction"
name="VaultTimeoutActions"
formControlName="vaultTimeoutAction"
>
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
{{ action | i18n }}
</option>
</select>
</div>
<div
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
id="unlockMethodHelp"
class="box-footer"
>
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
</div>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "otherOptions" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="fingerprint()"
>
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="twoStep()"
>
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="changePassword()"
*ngIf="showChangeMasterPass"
>
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="lock()"
>
<div class="row-main">{{ "lockNow" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="!accountSwitcherEnabled"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="logOut()"
>
<div class="row-main">{{ "logOut" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@ -1,6 +1,5 @@
import { ChangeDetectorRef, Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import {
BehaviorSubject,
combineLatest,
@ -17,13 +16,13 @@ import {
} from "rxjs";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -34,35 +33,20 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { DialogService } from "@bitwarden/components";
import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
import { BrowserApi } from "../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../platform/flags";
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { SetPinComponent } from "../components/set-pin.component";
import { AboutComponent } from "./about.component";
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
const RateUrls = {
[DeviceType.ChromeExtension]:
"https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
[DeviceType.FirefoxExtension]:
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews",
[DeviceType.OperaExtension]:
"https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container",
[DeviceType.EdgeExtension]:
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
[DeviceType.VivaldiExtension]:
"https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
[DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147",
};
@Component({
selector: "app-settings",
templateUrl: "settings.component.html",
selector: "auth-account-security",
templateUrl: "account-security.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SettingsComponent implements OnInit {
export class AccountSecurityComponent implements OnInit {
protected readonly VaultTimeoutAction = VaultTimeoutAction;
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
@ -88,6 +72,7 @@ export class SettingsComponent implements OnInit {
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private policyService: PolicyService,
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
@ -95,7 +80,6 @@ export class SettingsComponent implements OnInit {
private vaultTimeoutService: VaultTimeoutService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
public messagingService: MessagingService,
private router: Router,
private environmentService: EnvironmentService,
private cryptoService: CryptoService,
private stateService: StateService,
@ -149,7 +133,6 @@ export class SettingsComponent implements OnInit {
if (timeout === -2 && !showOnLocked) {
timeout = -1;
}
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
this.form.controls.vaultTimeout.valueChanges
.pipe(
@ -171,12 +154,14 @@ export class SettingsComponent implements OnInit {
)
.subscribe();
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const initialValues = {
vaultTimeout: timeout,
vaultTimeoutAction: await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
),
pin: pinStatus !== "DISABLED",
pin: await this.pinService.isPinSet(userId),
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
enableAutoBiometricsPrompt: await firstValueFrom(
this.biometricStateService.promptAutomatically$,
@ -425,23 +410,6 @@ export class SettingsComponent implements OnInit {
);
}
async lock() {
await this.vaultTimeoutService.lock();
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
type: "info",
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (confirmed) {
this.messagingService.send("logout", { userId: userId });
}
}
async changePassword() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "continueToWebApp" },
@ -468,44 +436,6 @@ export class SettingsComponent implements OnInit {
}
}
async share() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "learnOrg" },
content: { key: "learnOrgConfirmation" },
type: "info",
});
if (confirmed) {
// 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
BrowserApi.createNewTab("https://bitwarden.com/help/about-organizations/");
}
}
async webVault() {
const env = await firstValueFrom(this.environmentService.environment$);
const url = env.getWebVaultUrl();
await BrowserApi.createNewTab(url);
}
async import() {
await this.router.navigate(["/import"]);
if (await BrowserApi.isPopupOpen()) {
// 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
BrowserPopupUtils.openCurrentPagePopout(window);
}
}
export() {
// 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.router.navigate(["/export"]);
}
about() {
this.dialogService.open(AboutComponent);
}
async fingerprint() {
const fingerprint = await this.cryptoService.getFingerprint(
await this.stateService.getUserId(),
@ -518,11 +448,21 @@ export class SettingsComponent implements OnInit {
return firstValueFrom(dialogRef.closed);
}
rate() {
const deviceType = this.platformUtilsService.getDevice();
// 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
BrowserApi.createNewTab((RateUrls as any)[deviceType]);
async lock() {
await this.vaultTimeoutService.lock();
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
type: "info",
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (confirmed) {
this.messagingService.send("logout", { userId: userId });
}
}
ngOnDestroy() {

View File

@ -1,7 +1,7 @@
<form #form (ngSubmit)="submit()">
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<button type="button" routerLink="/notifications">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>

View File

@ -8,8 +8,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../platform/flags";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
interface ExcludedDomain {
uri: string;

View File

@ -0,0 +1,89 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "notifications" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="use-passkeys">{{ "enableUsePasskeys" | i18n }}</label>
<input
id="use-passkeys"
type="checkbox"
aria-describedby="use-passkeysHelp"
(change)="updateEnablePasskeys()"
[(ngModel)]="enablePasskeys"
/>
</div>
</div>
<div id="use-passkeysHelp" class="box-footer">
{{ "usePasskeysDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="addlogin-notification-bar">{{ "enableAddLoginNotification" | i18n }}</label>
<input
id="addlogin-notification-bar"
type="checkbox"
aria-describedby="addlogin-notification-barHelp"
(change)="updateAddLoginNotification()"
[(ngModel)]="enableAddLoginNotification"
/>
</div>
</div>
<div id="addlogin-notification-barHelp" class="box-footer">
{{
accountSwitcherEnabled
? ("addLoginNotificationDescAlt" | i18n)
: ("addLoginNotificationDesc" | i18n)
}}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="changedpass-notification-bar">{{
"enableChangedPasswordNotification" | i18n
}}</label>
<input
id="changedpass-notification-bar"
type="checkbox"
aria-describedby="changedpass-notification-barHelp"
(change)="updateChangedPasswordNotification()"
[(ngModel)]="enableChangedPasswordNotification"
/>
</div>
</div>
<div id="changedpass-notification-barHelp" class="box-footer">
{{
accountSwitcherEnabled
? ("changedPasswordNotificationDescAlt" | i18n)
: ("changedPasswordNotificationDesc" | i18n)
}}
</div>
</div>
<div class="box list">
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/excluded-domains"
>
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@ -0,0 +1,53 @@
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { enableAccountSwitching } from "../../../platform/flags";
@Component({
selector: "autofill-notification-settings",
templateUrl: "notifications.component.html",
})
export class NotifcationsSettingsComponent implements OnInit {
enableAddLoginNotification = false;
enableChangedPasswordNotification = false;
enablePasskeys = true;
accountSwitcherEnabled = false;
constructor(
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private vaultSettingsService: VaultSettingsService,
) {
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
this.enableAddLoginNotification = await firstValueFrom(
this.userNotificationSettingsService.enableAddedLoginPrompt$,
);
this.enableChangedPasswordNotification = await firstValueFrom(
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
);
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
}
async updateAddLoginNotification() {
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
this.enableAddLoginNotification,
);
}
async updateChangedPasswordNotification() {
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
this.enableChangedPasswordNotification,
);
}
async updateEnablePasskeys() {
await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys);
}
}

View File

@ -1,8 +1,8 @@
import { Subject, firstValueFrom, map, merge, timeout } from "rxjs";
import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs";
import {
PinCryptoServiceAbstraction,
PinCryptoService,
PinServiceAbstraction,
PinService,
InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService,
AuthRequestServiceAbstraction,
@ -84,7 +84,6 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
@ -246,10 +245,9 @@ export default class MainBackground {
messagingService: MessageSender;
storageService: BrowserLocalStorageService;
secureStorageService: AbstractStorageService;
memoryStorageService: AbstractMemoryStorageService;
memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService;
largeObjectMemoryStorageForStateProviders: AbstractMemoryStorageService &
ObservableStorageService;
memoryStorageService: AbstractStorageService;
memoryStorageForStateProviders: AbstractStorageService & ObservableStorageService;
largeObjectMemoryStorageForStateProviders: AbstractStorageService & ObservableStorageService;
i18nService: I18nServiceAbstraction;
platformUtilsService: PlatformUtilsServiceAbstraction;
logService: LogServiceAbstraction;
@ -320,7 +318,7 @@ export default class MainBackground {
authRequestService: AuthRequestServiceAbstraction;
accountService: AccountServiceAbstraction;
globalStateProvider: GlobalStateProvider;
pinCryptoService: PinCryptoServiceAbstraction;
pinService: PinServiceAbstraction;
singleUserStateProvider: SingleUserStateProvider;
activeUserStateProvider: ActiveUserStateProvider;
derivedStateProvider: DerivedStateProvider;
@ -544,13 +542,31 @@ export default class MainBackground {
const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
this.keyGenerationService,
this.encryptService,
);
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.pinService = new PinService(
this.accountService,
this.cryptoFunctionService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.masterPasswordService,
this.stateProvider,
this.stateService,
);
this.cryptoService = new BrowserCryptoService(
this.pinService,
this.masterPasswordService,
this.keyGenerationService,
this.cryptoFunctionService,
@ -631,6 +647,7 @@ export default class MainBackground {
this.stateProvider,
this.secureStorageService,
this.userDecryptionOptionsService,
this.logService,
);
this.devicesService = new DevicesServiceImplementation(this.devicesApiService);
@ -694,6 +711,8 @@ export default class MainBackground {
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.accountService,
this.pinService,
this.userDecryptionOptionsService,
this.cryptoService,
this.tokenService,
@ -702,14 +721,6 @@ export default class MainBackground {
this.biometricStateService,
);
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
this.kdfConfigService,
);
this.userVerificationService = new UserVerificationService(
this.stateService,
this.cryptoService,
@ -718,7 +729,7 @@ export default class MainBackground {
this.i18nService,
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinCryptoService,
this.pinService,
this.logService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
@ -840,11 +851,13 @@ export default class MainBackground {
this.i18nService,
this.collectionService,
this.cryptoService,
this.pinService,
);
this.individualVaultExportService = new IndividualVaultExportService(
this.folderService,
this.cipherService,
this.pinService,
this.cryptoService,
this.cryptoFunctionService,
this.kdfConfigService,
@ -853,6 +866,7 @@ export default class MainBackground {
this.organizationVaultExportService = new OrganizationVaultExportService(
this.cipherService,
this.apiService,
this.pinService,
this.cryptoService,
this.cryptoFunctionService,
this.collectionService,
@ -903,6 +917,7 @@ export default class MainBackground {
};
this.systemService = new SystemService(
this.pinService,
this.messagingService,
this.platformUtilsService,
systemUtilsServiceReloadCallback,
@ -1200,31 +1215,46 @@ export default class MainBackground {
}
async logout(expired: boolean, userId?: UserId) {
userId ??= (
await firstValueFrom(
this.accountService.activeAccount$.pipe(
timeout({
first: 2000,
with: () => {
throw new Error("No active account found to logout");
},
}),
),
)
)?.id;
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(
map((a) => a?.id),
timeout({
first: 2000,
with: () => {
throw new Error("No active account found to logout");
},
}),
),
);
await this.eventUploadService.uploadEvents(userId as UserId);
const userBeingLoggedOut = userId ?? activeUserId;
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
// HACK: We shouldn't wait for the authentication status to change but instead subscribe to the
// authentication status to do various actions.
const logoutPromise = firstValueFrom(
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
timeout({
first: 5_000,
with: () => {
throw new Error("The logout process did not complete in a reasonable amount of time.");
},
}),
),
);
await Promise.all([
this.syncService.setLastSync(new Date(0), userId),
this.cryptoService.clearKeys(userId),
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.passwordGenerationService.clear(userId),
this.vaultTimeoutSettingsService.clear(userId),
this.syncService.setLastSync(new Date(0), userBeingLoggedOut),
this.cryptoService.clearKeys(userBeingLoggedOut),
this.cipherService.clear(userBeingLoggedOut),
this.folderService.clear(userBeingLoggedOut),
this.collectionService.clear(userBeingLoggedOut),
this.passwordGenerationService.clear(userBeingLoggedOut),
this.vaultTimeoutSettingsService.clear(userBeingLoggedOut),
this.vaultFilterService.clear(),
this.biometricStateService.logout(userId),
this.biometricStateService.logout(userBeingLoggedOut),
/* We intentionally do not clear:
* - autofillSettingsService
* - badgeSettingsService
@ -1235,20 +1265,28 @@ export default class MainBackground {
//Needs to be checked before state is cleaned
const needStorageReseed = await this.needsStorageReseed();
const newActiveUser = await firstValueFrom(
this.accountService.nextUpAccount$.pipe(map((a) => a?.id)),
);
await this.stateService.clean({ userId: userId });
await this.accountService.clean(userId);
const newActiveUser =
userBeingLoggedOut === activeUserId
? await firstValueFrom(this.accountService.nextUpAccount$.pipe(map((a) => a?.id)))
: null;
await this.stateEventRunnerService.handleEvent("logout", userId);
await this.stateService.clean({ userId: userBeingLoggedOut });
await this.accountService.clean(userBeingLoggedOut);
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
await logoutPromise;
await this.switchAccount(newActiveUser);
if (newActiveUser != null) {
// we have a new active user, do not continue tearing down application
await this.switchAccount(newActiveUser as UserId);
this.messagingService.send("switchAccountFinish");
} else {
this.messagingService.send("doneLoggingOut", { expired: expired, userId: userId });
this.messagingService.send("doneLoggingOut", {
expired: expired,
userId: userBeingLoggedOut,
});
}
if (needStorageReseed) {

View File

@ -356,7 +356,7 @@ export class NativeMessagingBackground {
const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(message.keyB64),
) as MasterKey;
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterKey,
encUserKey,
);

View File

@ -8,6 +8,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherType } from "@bitwarden/common/vault/enums";
@ -324,9 +325,10 @@ export default class RuntimeBackground {
if (this.onInstalledReason != null) {
if (this.onInstalledReason === "install") {
// 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
BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
if (!devFlagEnabled("skipWelcomeOnInstall")) {
void BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
}
await this.autofillSettingsService.setInlineMenuVisibility(
AutofillOverlayVisibility.OnFieldFocus,
);

View File

@ -5,6 +5,14 @@ import {
policyServiceFactory,
PolicyServiceInitOptions,
} from "../../admin-console/background/service-factories/policy-service.factory";
import {
accountServiceFactory,
AccountServiceInitOptions,
} from "../../auth/background/service-factories/account-service.factory";
import {
pinServiceFactory,
PinServiceInitOptions,
} from "../../auth/background/service-factories/pin-service.factory";
import {
tokenServiceFactory,
TokenServiceInitOptions,
@ -34,6 +42,8 @@ import {
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
AccountServiceInitOptions &
PinServiceInitOptions &
UserDecryptionOptionsServiceInitOptions &
CryptoServiceInitOptions &
TokenServiceInitOptions &
@ -51,6 +61,8 @@ export function vaultTimeoutSettingsServiceFactory(
opts,
async () =>
new VaultTimeoutSettingsService(
await accountServiceFactory(cache, opts),
await pinServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts),

View File

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

View File

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

View File

@ -12,6 +12,10 @@ import {
internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions,
} from "../../../auth/background/service-factories/master-password-service.factory";
import {
PinServiceInitOptions,
pinServiceFactory,
} from "../../../auth/background/service-factories/pin-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
@ -45,6 +49,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider
type CryptoServiceFactoryOptions = FactoryOptions;
export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
PinServiceInitOptions &
MasterPasswordServiceInitOptions &
KeyGenerationServiceInitOptions &
CryptoFunctionServiceInitOptions &
@ -67,6 +72,7 @@ export function cryptoServiceFactory(
opts,
async () =>
new BrowserCryptoService(
await pinServiceFactory(cache, opts),
await internalMasterPasswordServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts),
await cryptoFunctionServiceFactory(cache, opts),

View File

@ -1,5 +1,4 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
@ -66,9 +65,9 @@ export function sessionStorageServiceFactory(
}
export function memoryStorageServiceFactory(
cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices,
cache: { memoryStorageService?: AbstractStorageService } & CachedServices,
opts: MemoryStorageServiceInitOptions,
): Promise<AbstractMemoryStorageService> {
): Promise<AbstractStorageService> {
return factory(cache, "memoryStorageService", opts, async () => {
if (BrowserApi.isManifestVersion(3)) {
return new LocalBackedSessionStorageService(
@ -97,10 +96,10 @@ export function memoryStorageServiceFactory(
export function observableMemoryStorageServiceFactory(
cache: {
memoryStorageService?: AbstractMemoryStorageService & ObservableStorageService;
memoryStorageService?: AbstractStorageService & ObservableStorageService;
} & CachedServices,
opts: MemoryStorageServiceInitOptions,
): Promise<AbstractMemoryStorageService & ObservableStorageService> {
): Promise<AbstractStorageService & ObservableStorageService> {
return factory(cache, "memoryStorageService", opts, async () => {
return new BackgroundMemoryStorageService();
});

View File

@ -1,88 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { DefaultBrowserStateService } from "../../services/default-browser-state.service";
import { browserSession } from "./browser-session.decorator";
import { SessionStorable } from "./session-storable";
import { sessionSync } from "./session-sync.decorator";
// browserSession initializes SessionSyncers for each sessionSync decorated property
// We don't want to test SessionSyncers, so we'll mock them
jest.mock("./session-syncer");
describe("browserSession decorator", () => {
it("should throw if neither StateService nor MemoryStorageService is a constructor argument", () => {
@browserSession
class TestClass {}
expect(() => {
new TestClass();
}).toThrowError(
"Cannot decorate TestClass with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters",
);
});
it("should create if StateService is a constructor argument", () => {
const stateService = Object.create(DefaultBrowserStateService.prototype, {
memoryStorageService: {
value: Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
}),
},
});
@browserSession
class TestClass {
constructor(private stateService: DefaultBrowserStateService) {}
}
expect(new TestClass(stateService)).toBeDefined();
});
it("should create if MemoryStorageService is a constructor argument", () => {
const memoryStorageService = Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
});
@browserSession
class TestClass {
constructor(private memoryStorageService: AbstractMemoryStorageService) {}
}
expect(new TestClass(memoryStorageService)).toBeDefined();
});
describe("interaction with @sessionSync decorator", () => {
let memoryStorageService: MemoryStorageService;
@browserSession
class TestClass {
@sessionSync({ initializer: (s: string) => s })
private behaviorSubject = new BehaviorSubject("");
constructor(private memoryStorageService: MemoryStorageService) {}
fromJSON(json: any) {
this.behaviorSubject.next(json);
}
}
beforeEach(() => {
memoryStorageService = Object.create(MemoryStorageService.prototype, {
type: { value: MemoryStorageService.TYPE },
});
});
it("should create a session syncer", () => {
const testClass = new TestClass(memoryStorageService) as any as SessionStorable;
expect(testClass.__sessionSyncers.length).toEqual(1);
});
it("should initialize the session syncer", () => {
const testClass = new TestClass(memoryStorageService) as any as SessionStorable;
expect(testClass.__sessionSyncers[0].init).toHaveBeenCalled();
});
});
});

View File

@ -1,75 +0,0 @@
import { Constructor } from "type-fest";
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { SessionStorable } from "./session-storable";
import { SessionSyncer } from "./session-syncer";
import { SyncedItemMetadata } from "./sync-item-metadata";
/**
* Mark the class as syncing state across the browser session. This decorator finds rxjs BehaviorSubject properties
* marked with @sessionSync and syncs these values across the browser session.
*
* @param constructor
* @returns A new constructor that extends the original one to add session syncing.
*/
export function browserSession<TCtor extends Constructor<any>>(constructor: TCtor) {
return class extends constructor implements SessionStorable {
__syncedItemMetadata: SyncedItemMetadata[];
__sessionSyncers: SessionSyncer[];
constructor(...args: any[]) {
super(...args);
// Require state service to be injected
const storageService: AbstractMemoryStorageService = this.findStorageService(
[this as any].concat(args),
);
if (this.__syncedItemMetadata == null || !(this.__syncedItemMetadata instanceof Array)) {
return;
}
this.__sessionSyncers = this.__syncedItemMetadata.map((metadata) =>
this.buildSyncer(metadata, storageService),
);
}
buildSyncer(metadata: SyncedItemMetadata, storageSerice: AbstractMemoryStorageService) {
const syncer = new SessionSyncer(
(this as any)[metadata.propertyKey],
storageSerice,
metadata,
);
// 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
syncer.init();
return syncer;
}
findStorageService(args: any[]): AbstractMemoryStorageService {
const storageService = args.find(this.isMemoryStorageService);
if (storageService) {
return storageService;
}
const stateService = args.find(
(arg) =>
arg?.memoryStorageService != null &&
this.isMemoryStorageService(arg.memoryStorageService),
);
if (stateService) {
return stateService.memoryStorageService;
}
throw new Error(
`Cannot decorate ${constructor.name} with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters`,
);
}
isMemoryStorageService(arg: any): arg is AbstractMemoryStorageService {
return arg.type != null && arg.type === AbstractMemoryStorageService.TYPE;
}
};
}

View File

@ -1,2 +0,0 @@
export { browserSession } from "./browser-session.decorator";
export { sessionSync } from "./session-sync.decorator";

View File

@ -1,7 +0,0 @@
import { SessionSyncer } from "./session-syncer";
import { SyncedItemMetadata } from "./sync-item-metadata";
export interface SessionStorable {
__syncedItemMetadata: SyncedItemMetadata[];
__sessionSyncers: SessionSyncer[];
}

View File

@ -1,57 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { sessionSync } from "./session-sync.decorator";
describe("sessionSync decorator", () => {
const initializer = (s: string) => "test";
class TestClass {
@sessionSync({ initializer: initializer })
private testProperty = new BehaviorSubject("");
@sessionSync({ initializer: initializer, initializeAs: "array" })
private secondTestProperty = new BehaviorSubject("");
complete() {
this.testProperty.complete();
this.secondTestProperty.complete();
}
}
it("should add __syncedItemKeys to prototype", () => {
const testClass = new TestClass();
expect((testClass as any).__syncedItemMetadata).toEqual([
expect.objectContaining({
propertyKey: "testProperty",
sessionKey: "testProperty_0",
initializer: initializer,
}),
expect.objectContaining({
propertyKey: "secondTestProperty",
sessionKey: "secondTestProperty_1",
initializer: initializer,
initializeAs: "array",
}),
]);
testClass.complete();
});
class TestClass2 {
@sessionSync({ initializer: initializer })
private testProperty = new BehaviorSubject("");
complete() {
this.testProperty.complete();
}
}
it("should maintain sessionKey index count for other test classes", () => {
const testClass = new TestClass2();
expect((testClass as any).__syncedItemMetadata).toEqual([
expect.objectContaining({
propertyKey: "testProperty",
sessionKey: "testProperty_2",
initializer: initializer,
}),
]);
testClass.complete();
});
});

View File

@ -1,54 +0,0 @@
import { Jsonify } from "type-fest";
import { SessionStorable } from "./session-storable";
import { InitializeOptions } from "./sync-item-metadata";
class BuildOptions<T, TJson = Jsonify<T>> {
initializer?: (keyValuePair: TJson) => T;
initializeAs?: InitializeOptions;
}
// Used to ensure uniqueness for each synced observable
let index = 0;
/**
* A decorator used to indicate the BehaviorSubject should be synced for this browser session across all contexts.
*
* >**Note** This decorator does nothing if the enclosing class is not decorated with @browserSession.
*
* >**Note** The Behavior subject must be initialized with a default or in the constructor of the class. If it is not, an error will be thrown.
*
* >**!!Warning!!** If the property is overwritten at any time, the new value will not be synced across the browser session.
*
* @param buildOptions
* Builders for the value, requires either a constructor (ctor) for your BehaviorSubject type or an
* initializer function that takes a key value pair representation of the BehaviorSubject data
* and returns your instantiated BehaviorSubject value. `initializeAs can optionally be used to indicate
* the provided initializer function should be used to build an array of values. For example,
* ```ts
* \@sessionSync({ initializer: Foo.fromJSON, initializeAs: 'array' })
* ```
* is equivalent to
* ```
* \@sessionSync({ initializer: (obj: any[]) => obj.map((f) => Foo.fromJSON })
* ```
*
* @returns decorator function
*/
export function sessionSync<T>(buildOptions: BuildOptions<T>) {
return (prototype: unknown, propertyKey: string) => {
// Force prototype into SessionStorable and implement it.
const p = prototype as SessionStorable;
if (p.__syncedItemMetadata == null) {
p.__syncedItemMetadata = [];
}
p.__syncedItemMetadata.push({
propertyKey,
sessionKey: `${propertyKey}_${index++}`,
initializer: buildOptions.initializer,
initializeAs: buildOptions.initializeAs ?? "object",
});
};
}

View File

@ -1,301 +0,0 @@
import { awaitAsync } from "@bitwarden/common/../spec/utils";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, ReplaySubject } from "rxjs";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { BrowserApi } from "../../browser/browser-api";
import { SessionSyncer } from "./session-syncer";
import { SyncedItemMetadata } from "./sync-item-metadata";
describe("session syncer", () => {
const propertyKey = "behaviorSubject";
const sessionKey = "Test__" + propertyKey;
const metaData: SyncedItemMetadata = {
propertyKey,
sessionKey,
initializer: (s: string) => s,
initializeAs: "object",
};
let storageService: MockProxy<MemoryStorageService>;
let sut: SessionSyncer;
let behaviorSubject: BehaviorSubject<string>;
beforeEach(() => {
behaviorSubject = new BehaviorSubject<string>("");
jest.spyOn(chrome.runtime, "getManifest").mockReturnValue({
name: "bitwarden-test",
version: "0.0.0",
manifest_version: 3,
});
storageService = mock();
storageService.has.mockResolvedValue(false);
sut = new SessionSyncer(behaviorSubject, storageService, metaData);
});
afterEach(() => {
jest.resetAllMocks();
behaviorSubject.complete();
});
describe("constructor", () => {
it("should throw if subject is not an instance of Subject", () => {
expect(() => {
new SessionSyncer({} as any, storageService, null);
}).toThrowError("subject must inherit from Subject");
});
it("should create if either ctor or initializer is provided", () => {
expect(
new SessionSyncer(behaviorSubject, storageService, {
propertyKey,
sessionKey,
initializeAs: "object",
initializer: () => null,
}),
).toBeDefined();
expect(
new SessionSyncer(behaviorSubject, storageService, {
propertyKey,
sessionKey,
initializer: (s: any) => s,
initializeAs: "object",
}),
).toBeDefined();
});
it("should throw if neither ctor or initializer is provided", () => {
expect(() => {
new SessionSyncer(behaviorSubject, storageService, {
propertyKey,
sessionKey,
initializeAs: "object",
initializer: null,
});
}).toThrowError("initializer must be provided");
});
});
describe("init", () => {
it("should ignore all updates currently in a ReplaySubject's buffer", () => {
const replaySubject = new ReplaySubject<string>(Infinity);
replaySubject.next("1");
replaySubject.next("2");
replaySubject.next("3");
sut = new SessionSyncer(replaySubject, storageService, metaData);
// block observing the subject
jest.spyOn(sut as any, "observe").mockImplementation();
// 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
sut.init();
expect(sut["ignoreNUpdates"]).toBe(3);
});
it("should ignore BehaviorSubject's initial value", () => {
const behaviorSubject = new BehaviorSubject<string>("initial");
sut = new SessionSyncer(behaviorSubject, storageService, metaData);
// block observing the subject
jest.spyOn(sut as any, "observe").mockImplementation();
// 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
sut.init();
expect(sut["ignoreNUpdates"]).toBe(1);
});
it("should grab an initial value from storage if it exists", async () => {
storageService.has.mockResolvedValue(true);
//Block a call to update
const updateSpy = jest.spyOn(sut as any, "updateFromMemory").mockImplementation();
// 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
sut.init();
await awaitAsync();
expect(updateSpy).toHaveBeenCalledWith();
});
it("should not grab an initial value from storage if it does not exist", async () => {
storageService.has.mockResolvedValue(false);
//Block a call to update
const updateSpy = jest.spyOn(sut as any, "update").mockImplementation();
// 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
sut.init();
await awaitAsync();
expect(updateSpy).not.toHaveBeenCalled();
});
});
describe("a value is emitted on the observable", () => {
let sendMessageSpy: jest.SpyInstance;
const value = "test";
const serializedValue = JSON.stringify(value);
beforeEach(() => {
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
// 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
sut.init();
behaviorSubject.next(value);
});
it("should update sessionSyncers in other contexts", async () => {
// await finishing of fire-and-forget operation
await new Promise((resolve) => setTimeout(resolve, 100));
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
expect(sendMessageSpy).toHaveBeenCalledWith(`${sessionKey}_update`, {
id: sut.id,
serializedValue,
});
});
});
describe("A message is received", () => {
let nextSpy: jest.SpyInstance;
let sendMessageSpy: jest.SpyInstance;
beforeEach(() => {
nextSpy = jest.spyOn(behaviorSubject, "next");
sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage");
// 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
sut.init();
});
afterEach(() => {
jest.resetAllMocks();
});
it("should ignore messages with the wrong command", async () => {
await sut.updateFromMessage({ command: "wrong_command", id: sut.id });
expect(storageService.getBypassCache).not.toHaveBeenCalled();
expect(nextSpy).not.toHaveBeenCalled();
});
it("should ignore messages from itself", async () => {
await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id });
expect(storageService.getBypassCache).not.toHaveBeenCalled();
expect(nextSpy).not.toHaveBeenCalled();
});
it("should update from message on emit from another instance", async () => {
const builder = jest.fn();
jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder);
const value = "test";
const serializedValue = JSON.stringify(value);
builder.mockReturnValue(value);
// Expect no circular messaging
await awaitAsync();
expect(sendMessageSpy).toHaveBeenCalledTimes(0);
await sut.updateFromMessage({
command: `${sessionKey}_update`,
id: "different_id",
serializedValue,
});
await awaitAsync();
expect(storageService.getBypassCache).toHaveBeenCalledTimes(0);
expect(nextSpy).toHaveBeenCalledTimes(1);
expect(nextSpy).toHaveBeenCalledWith(value);
expect(behaviorSubject.value).toBe(value);
// Expect no circular messaging
expect(sendMessageSpy).toHaveBeenCalledTimes(0);
});
});
describe("memory storage", () => {
const value = "test";
const serializedValue = JSON.stringify(value);
let saveSpy: jest.SpyInstance;
const builder = jest.fn().mockReturnValue(value);
const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
const isBackgroundPageSpy = jest.spyOn(BrowserApi, "isBackgroundPage");
beforeEach(async () => {
jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder);
saveSpy = jest.spyOn(storageService, "save");
// 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
sut.init();
await awaitAsync();
});
afterEach(() => {
jest.resetAllMocks();
});
it("should always store on observed next for manifest version 3", async () => {
manifestVersionSpy.mockReturnValue(3);
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
behaviorSubject.next(value);
await awaitAsync();
behaviorSubject.next(value);
await awaitAsync();
expect(saveSpy).toHaveBeenCalledTimes(2);
});
it("should not store on message receive for manifest version 3", async () => {
manifestVersionSpy.mockReturnValue(3);
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
await sut.updateFromMessage({
command: `${sessionKey}_update`,
id: "different_id",
serializedValue,
});
await awaitAsync();
expect(saveSpy).toHaveBeenCalledTimes(0);
});
it("should store on message receive for manifest version 2 for background page only", async () => {
manifestVersionSpy.mockReturnValue(2);
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
await sut.updateFromMessage({
command: `${sessionKey}_update`,
id: "different_id",
serializedValue,
});
await awaitAsync();
await sut.updateFromMessage({
command: `${sessionKey}_update`,
id: "different_id",
serializedValue,
});
await awaitAsync();
expect(saveSpy).toHaveBeenCalledTimes(1);
});
it("should store on observed next for manifest version 2 for background page only", async () => {
manifestVersionSpy.mockReturnValue(2);
isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false);
behaviorSubject.next(value);
await awaitAsync();
behaviorSubject.next(value);
await awaitAsync();
expect(saveSpy).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -1,125 +0,0 @@
import { BehaviorSubject, concatMap, ReplaySubject, skip, Subject, Subscription } from "rxjs";
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../../browser/browser-api";
import { SyncedItemMetadata } from "./sync-item-metadata";
export class SessionSyncer {
subscription: Subscription;
id = Utils.newGuid();
// ignore initial values
private ignoreNUpdates = 0;
constructor(
private subject: Subject<any>,
private memoryStorageService: AbstractMemoryStorageService,
private metaData: SyncedItemMetadata,
) {
if (!(subject instanceof Subject)) {
throw new Error("subject must inherit from Subject");
}
if (metaData.initializer == null) {
throw new Error("initializer must be provided");
}
}
async init() {
switch (this.subject.constructor) {
case ReplaySubject:
// ignore all updates currently in the buffer
this.ignoreNUpdates = (this.subject as any)._buffer.length;
break;
case BehaviorSubject:
this.ignoreNUpdates = 1;
break;
default:
break;
}
await this.observe();
// must be synchronous
const hasInSessionMemory = await this.memoryStorageService.has(this.metaData.sessionKey);
if (hasInSessionMemory) {
await this.updateFromMemory();
}
this.listenForUpdates();
}
private async observe() {
const stream = this.subject.pipe(skip(this.ignoreNUpdates));
this.ignoreNUpdates = 0;
// This may be a memory leak.
// There is no good time to unsubscribe from this observable. Hopefully Manifest V3 clears memory from temporary
// contexts. If so, this is handled by destruction of the context.
this.subscription = stream
.pipe(
concatMap(async (next) => {
if (this.ignoreNUpdates > 0) {
this.ignoreNUpdates -= 1;
return;
}
await this.updateSession(next);
}),
)
.subscribe();
}
private listenForUpdates() {
// This is an unawaited promise, but it will be executed asynchronously in the background.
BrowserApi.messageListener(this.updateMessageCommand, (message) => {
// 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.updateFromMessage(message);
});
}
async updateFromMessage(message: any) {
if (message.command != this.updateMessageCommand || message.id === this.id) {
return;
}
await this.update(message.serializedValue);
}
async updateFromMemory() {
const value = await this.memoryStorageService.getBypassCache(this.metaData.sessionKey);
await this.update(value);
}
async update(serializedValue: any) {
if (!serializedValue) {
return;
}
const unBuiltValue = JSON.parse(serializedValue);
if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) {
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
}
const builder = SyncedItemMetadata.builder(this.metaData);
const value = builder(unBuiltValue);
this.ignoreNUpdates = 1;
this.subject.next(value);
}
private async updateSession(value: any) {
if (!value) {
return;
}
const serializedValue = JSON.stringify(value);
if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) {
await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue);
}
await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id, serializedValue });
}
private get updateMessageCommand() {
return `${this.metaData.sessionKey}_update`;
}
}

View File

@ -1,25 +0,0 @@
export type InitializeOptions = "array" | "record" | "object";
export class SyncedItemMetadata {
propertyKey: string;
sessionKey: string;
initializer: (keyValuePair: any) => any;
initializeAs: InitializeOptions;
static builder(metadata: SyncedItemMetadata): (o: any) => any {
const itemBuilder = metadata.initializer;
if (metadata.initializeAs === "array") {
return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o));
} else if (metadata.initializeAs === "record") {
return (keyValuePair: any) => {
const record: Record<any, any> = {};
for (const key in keyValuePair) {
record[key] = itemBuilder(keyValuePair[key]);
}
return record;
};
} else {
return (keyValuePair: any) => itemBuilder(keyValuePair);
}
}
}

View File

@ -1,42 +0,0 @@
import { SyncedItemMetadata } from "./sync-item-metadata";
describe("builder", () => {
const propertyKey = "propertyKey";
const key = "key";
const initializer = (s: any) => "used initializer";
it("should use initializer", () => {
const metadata: SyncedItemMetadata = {
propertyKey,
sessionKey: key,
initializer,
initializeAs: "object",
};
const builder = SyncedItemMetadata.builder(metadata);
expect(builder({})).toBe("used initializer");
});
it("should honor initialize as array", () => {
const metadata: SyncedItemMetadata = {
propertyKey,
sessionKey: key,
initializer: initializer,
initializeAs: "array",
};
const builder = SyncedItemMetadata.builder(metadata);
expect(builder([{}])).toBeInstanceOf(Array);
expect(builder([{}])[0]).toBe("used initializer");
});
it("should honor initialize as record", () => {
const metadata: SyncedItemMetadata = {
propertyKey,
sessionKey: key,
initializer: initializer,
initializeAs: "record",
};
const builder = SyncedItemMetadata.builder(metadata);
expect(builder({ key: "" })).toBeInstanceOf(Object);
expect(builder({ key: "" })).toStrictEqual({ key: "used initializer" });
});
});

View File

@ -315,13 +315,13 @@ export default {
importProvidersFrom(
RouterModule.forRoot(
[
{ path: "", redirectTo: "vault", pathMatch: "full" },
{ path: "vault", component: MockVaultPageComponent },
{ path: "generator", component: MockGeneratorPageComponent },
{ path: "send", component: MockSendPageComponent },
{ path: "settings", component: MockSettingsPageComponent },
{ path: "", redirectTo: "tabs/vault", pathMatch: "full" },
{ path: "tabs/vault", component: MockVaultPageComponent },
{ path: "tabs/generator", component: MockGeneratorPageComponent },
{ path: "tabs/send", component: MockSendPageComponent },
{ path: "tabs/settings", component: MockSettingsPageComponent },
// in case you are coming from a story that also uses the router
{ path: "**", redirectTo: "vault" },
{ path: "**", redirectTo: "tabs/vault" },
],
{ useHash: true },
),

View File

@ -17,25 +17,25 @@ export class PopupTabNavigationComponent {
navButtons = [
{
label: "Vault",
page: "/vault",
page: "/tabs/vault",
iconKey: "lock",
iconKeyActive: "lock-f",
},
{
label: "Generator",
page: "/generator",
page: "/tabs/generator",
iconKey: "generate",
iconKeyActive: "generate-f",
},
{
label: "Send",
page: "/send",
page: "/tabs/send",
iconKey: "send",
iconKeyActive: "send-f",
},
{
label: "Settings",
page: "/settings",
page: "/tabs/settings",
iconKey: "cog",
iconKeyActive: "cog-f",
},

View File

@ -1,5 +1,6 @@
import { firstValueFrom } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@ -19,6 +20,7 @@ import { UserKey } from "@bitwarden/common/types/key";
export class BrowserCryptoService extends CryptoService {
constructor(
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService,
@ -32,6 +34,7 @@ export class BrowserCryptoService extends CryptoService {
kdfConfigService: KdfConfigService,
) {
super(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,

View File

@ -1,16 +1,7 @@
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
export default class BrowserMemoryStorageService
extends AbstractChromeStorageService
implements AbstractMemoryStorageService
{
export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
constructor() {
super(chrome.storage.session);
}
type = "MemoryStorageService" as const;
getBypassCache<T>(key: string): Promise<T> {
return this.get(key);
}
}

View File

@ -3,10 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { State } from "@bitwarden/common/platform/models/domain/state";
@ -18,9 +15,6 @@ import { Account } from "../../models/account";
import { DefaultBrowserStateService } from "./default-browser-state.service";
// disable session syncing to just test class
jest.mock("../decorators/session-sync-observable/");
describe("Browser State Service", () => {
let secureStorageService: MockProxy<AbstractStorageService>;
let diskStorageService: MockProxy<AbstractStorageService>;
@ -56,7 +50,7 @@ describe("Browser State Service", () => {
});
describe("state methods", () => {
let memoryStorageService: MockProxy<AbstractMemoryStorageService>;
let memoryStorageService: MockProxy<AbstractStorageService>;
beforeEach(() => {
memoryStorageService = mock();

View File

@ -1,13 +1,8 @@
import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
AbstractStorageService,
AbstractMemoryStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
@ -15,27 +10,19 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
import { Account } from "../../models/account";
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
import { BrowserStateService } from "./abstractions/browser-state.service";
@browserSession
export class DefaultBrowserStateService
extends BaseStateService<GlobalState, Account>
implements BrowserStateService
{
@sessionSync({
initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account
initializeAs: "record",
})
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
protected accountDeserializer = Account.fromJSON;
constructor(
storageService: AbstractStorageService,
secureStorageService: AbstractStorageService,
memoryStorageService: AbstractMemoryStorageService,
memoryStorageService: AbstractStorageService,
logService: LogService,
stateFactory: StateFactory<GlobalState, Account>,
accountService: AccountService,

View File

@ -59,24 +59,12 @@ describe("LocalBackedSessionStorage", () => {
await sut.get("test");
expect(sut["cache"]["test"]).toEqual("decrypted");
});
});
describe("getBypassCache", () => {
it("ignores cached values", async () => {
sut["cache"]["test"] = "cached";
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.getBypassCache("test");
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
expect(result).toEqual("decrypted");
});
it("returns a decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.getBypassCache("test");
const result = await sut.get("test");
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
expect(result).toEqual("decrypted");
});
@ -85,19 +73,9 @@ describe("LocalBackedSessionStorage", () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
await sut.getBypassCache("test");
await sut.get("test");
expect(sut["cache"]["test"]).toEqual("decrypted");
});
it("deserializes when a deserializer is provided", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const deserializer = jest.fn().mockReturnValue("deserialized");
const result = await sut.getBypassCache("test", { deserializer });
expect(deserializer).toHaveBeenCalledWith("decrypted");
expect(result).toEqual("deserialized");
});
});
describe("has", () => {

View File

@ -1,18 +1,16 @@
import { Subject } from "rxjs";
import { Jsonify } from "type-fest";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BrowserApi } from "../browser/browser-api";
@ -20,7 +18,7 @@ import { MemoryStoragePortMessage } from "../storage/port-messages";
import { portName } from "../storage/port-name";
export class LocalBackedSessionStorageService
extends AbstractMemoryStorageService
extends AbstractStorageService
implements ObservableStorageService
{
private ports: Set<chrome.runtime.Port> = new Set([]);
@ -65,20 +63,12 @@ export class LocalBackedSessionStorageService
});
}
async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
async get<T>(key: string, options?: StorageOptions): Promise<T> {
if (this.cache[key] !== undefined) {
return this.cache[key] as T;
}
return await this.getBypassCache(key, options);
}
async getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
let value = await this.getLocalSessionValue(await this.sessionKey.get(), key);
if (options?.deserializer != null) {
value = options.deserializer(value as Jsonify<T>);
}
const value = await this.getLocalSessionValue(await this.sessionKey.get(), key);
this.cache[key] = value;
return value as T;
@ -159,7 +149,6 @@ export class LocalBackedSessionStorageService
switch (message.action) {
case "get":
case "getBypassCache":
case "has": {
result = await this[message.action](message.key);
break;

View File

@ -51,7 +51,6 @@ export class BackgroundMemoryStorageService extends MemoryStorageService {
switch (message.action) {
case "get":
case "getBypassCache":
case "has": {
result = await this[message.action](message.key);
break;

View File

@ -1,7 +1,7 @@
import { Observable, Subject, filter, firstValueFrom, map } from "rxjs";
import {
AbstractMemoryStorageService,
AbstractStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -11,7 +11,7 @@ import { fromChromeEvent } from "../browser/from-chrome-event";
import { MemoryStoragePortMessage } from "./port-messages";
import { portName } from "./port-name";
export class ForegroundMemoryStorageService extends AbstractMemoryStorageService {
export class ForegroundMemoryStorageService extends AbstractStorageService {
private _port: chrome.runtime.Port;
private _backgroundResponses$: Observable<MemoryStoragePortMessage>;
private updatesSubject = new Subject<StorageUpdate>();
@ -59,9 +59,6 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService
async get<T>(key: string): Promise<T> {
return await this.delegateToBackground<T>("get", key);
}
async getBypassCache<T>(key: string): Promise<T> {
return await this.delegateToBackground<T>("getBypassCache", key);
}
async has(key: string): Promise<boolean> {
return await this.delegateToBackground<boolean>("has", key);
}

View File

@ -25,9 +25,9 @@ describe("foreground background memory storage interaction", () => {
jest.resetAllMocks();
});
test.each(["has", "get", "getBypassCache"])(
test.each(["has", "get"])(
"background should respond with the correct value for %s",
async (action: "get" | "has" | "getBypassCache") => {
async (action: "get" | "has") => {
const key = "key";
const value = "value";
background[action] = jest.fn().mockResolvedValue(value);

View File

@ -1,5 +1,5 @@
import {
AbstractMemoryStorageService,
AbstractStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
@ -14,7 +14,7 @@ type MemoryStoragePortMessage = {
data: string | string[] | StorageUpdate;
originator: "foreground" | "background";
action?:
| keyof Pick<AbstractMemoryStorageService, "get" | "getBypassCache" | "has" | "save" | "remove">
| keyof Pick<AbstractStorageService, "get" | "has" | "save" | "remove">
| "subject_update"
| "initialization";
};

View File

@ -174,27 +174,35 @@ export const routerTransition = trigger("routerTransition", [
transition("clone-cipher => attachments, clone-cipher => collections", inSlideLeft),
transition("attachments => clone-cipher, collections => clone-cipher", outSlideRight),
transition("tabs => import", inSlideLeft),
transition("import => tabs", outSlideRight),
transition("tabs => account-security", inSlideLeft),
transition("account-security => tabs", outSlideRight),
transition("tabs => export", inSlideLeft),
transition("export => tabs", outSlideRight),
// Vault settings
transition("tabs => vault-settings", inSlideLeft),
transition("vault-settings => tabs", outSlideRight),
transition("tabs => folders", inSlideLeft),
transition("folders => tabs", outSlideRight),
transition("vault-settings => import", inSlideLeft),
transition("import => vault-settings", outSlideRight),
transition("vault-settings => export", inSlideLeft),
transition("export => vault-settings", outSlideRight),
transition("vault-settings => folders", inSlideLeft),
transition("folders => vault-settings", outSlideRight),
transition("folders => edit-folder, folders => add-folder", inSlideUp),
transition("edit-folder => folders, add-folder => folders", outSlideDown),
transition("tabs => sync", inSlideLeft),
transition("sync => tabs", outSlideRight),
transition("tabs => excluded-domains", inSlideLeft),
transition("excluded-domains => tabs", outSlideRight),
transition("vault-settings => sync", inSlideLeft),
transition("sync => vault-settings", outSlideRight),
transition("tabs => options", inSlideLeft),
transition("options => tabs", outSlideRight),
// Appearance settings
transition("tabs => appearance", inSlideLeft),
transition("appearance => tabs", outSlideRight),
transition("tabs => premium", inSlideLeft),
transition("premium => tabs", outSlideRight),
@ -212,6 +220,13 @@ export const routerTransition = trigger("routerTransition", [
transition("tabs => edit-send, send-type => edit-send", inSlideUp),
transition("edit-send => tabs, edit-send => send-type", outSlideDown),
// Notification settings
transition("tabs => notifications", inSlideLeft),
transition("notifications => tabs", outSlideRight),
transition("notifications => excluded-domains", inSlideLeft),
transition("excluded-domains => notifications", outSlideRight),
transition("tabs => autofill", inSlideLeft),
transition("autofill => tabs", outSlideRight),

View File

@ -2,9 +2,9 @@ import { Injectable, NgModule } from "@angular/core";
import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router";
import {
redirectGuard,
AuthGuard,
lockGuard,
redirectGuard,
tdeDecryptionRequiredGuard,
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
@ -21,11 +21,14 @@ import { LoginComponent } from "../auth/popup/login.component";
import { RegisterComponent } from "../auth/popup/register.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { SsoComponent } from "../auth/popup/sso.component";
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotifcationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PremiumComponent } from "../billing/popup/settings/premium.component";
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
@ -35,6 +38,7 @@ import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.compo
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
import { ExportComponent } from "../tools/popup/settings/export.component";
import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component";
import { SettingsComponent } from "../tools/popup/settings/settings.component";
import { Fido2Component } from "../vault/popup/components/fido2/fido2.component";
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
@ -44,16 +48,19 @@ import { PasswordHistoryComponent } from "../vault/popup/components/vault/passwo
import { ShareComponent } from "../vault/popup/components/vault/share.component";
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component";
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
import { FoldersComponent } from "./settings/folders.component";
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
import { OptionsComponent } from "./settings/options.component";
import { SettingsComponent } from "./settings/settings.component";
import { SyncComponent } from "./settings/sync.component";
import { TabsV2Component } from "./tabs-v2.component";
import { TabsComponent } from "./tabs.component";
const unauthRouteOverrides = {
@ -244,6 +251,24 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { state: "autofill" },
},
{
path: "account-security",
component: AccountSecurityComponent,
canActivate: [AuthGuard],
data: { state: "account-security" },
},
{
path: "notifications",
component: NotifcationsSettingsComponent,
canActivate: [AuthGuard],
data: { state: "notifications" },
},
{
path: "vault-settings",
component: VaultSettingsComponent,
canActivate: [AuthGuard],
data: { state: "vault-settings" },
},
{
path: "folders",
component: FoldersComponent,
@ -286,6 +311,12 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { state: "options" },
},
{
path: "appearance",
component: AppearanceComponent,
canActivate: [AuthGuard],
data: { state: "appearance" },
},
{
path: "clone-cipher",
component: AddEditComponent,
@ -322,9 +353,8 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { state: "help-and-feedback" },
},
{
...extensionRefreshSwap(TabsComponent, TabsV2Component, {
path: "tabs",
component: TabsComponent,
data: { state: "tabs" },
children: [
{
@ -336,15 +366,15 @@ const routes: Routes = [
path: "current",
component: CurrentTabComponent,
canActivate: [AuthGuard],
canMatch: [extensionRefreshRedirect("/tabs/vault")],
data: { state: "tabs_current" },
runGuardsAndResolvers: "always",
},
{
...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, {
path: "vault",
component: VaultFilterComponent,
canActivate: [AuthGuard],
data: { state: "tabs_vault" },
},
}),
{
path: "generator",
component: GeneratorComponent,
@ -364,7 +394,7 @@ const routes: Routes = [
data: { state: "tabs_send" },
},
],
},
}),
{
path: "account-switcher",
component: AccountSwitcherComponent,

View File

@ -30,11 +30,15 @@ import { LoginComponent } from "../auth/popup/login.component";
import { RegisterComponent } from "../auth/popup/register.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { SsoComponent } from "../auth/popup/sso.component";
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotifcationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PremiumComponent } from "../billing/popup/settings/premium.component";
import { HeaderComponent } from "../platform/popup/header.component";
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
@ -49,6 +53,7 @@ import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.componen
import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component";
import { SendTypeComponent } from "../tools/popup/send/send-type.component";
import { ExportComponent } from "../tools/popup/settings/export.component";
import { SettingsComponent } from "../tools/popup/settings/settings.component";
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component";
@ -64,22 +69,23 @@ import { ShareComponent } from "../vault/popup/components/vault/share.component"
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component";
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { PopOutComponent } from "./components/pop-out.component";
import { UserVerificationComponent } from "./components/user-verification.component";
import { ServicesModule } from "./services/services.module";
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
import { FoldersComponent } from "./settings/folders.component";
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
import { OptionsComponent } from "./settings/options.component";
import { SettingsComponent } from "./settings/settings.component";
import { SyncComponent } from "./settings/sync.component";
import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component";
import { TabsV2Component } from "./tabs-v2.component";
import { TabsComponent } from "./tabs.component";
// Register the locales for the application
@ -144,6 +150,8 @@ import "../platform/popup/locales";
LoginViaAuthRequestComponent,
LoginDecryptionOptionsComponent,
OptionsComponent,
NotifcationsSettingsComponent,
AppearanceComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordHistoryComponent,
@ -155,11 +163,14 @@ import "../platform/popup/locales";
SendListComponent,
SendTypeComponent,
SetPasswordComponent,
AccountSecurityComponent,
SettingsComponent,
VaultSettingsComponent,
ShareComponent,
SsoComponent,
SyncComponent,
TabsComponent,
TabsV2Component,
TwoFactorComponent,
TwoFactorOptionsComponent,
UpdateTempPasswordComponent,
@ -175,6 +186,7 @@ import "../platform/popup/locales";
EnvironmentSelectorComponent,
CurrentAccountComponent,
AccountSwitcherComponent,
VaultV2Component,
],
providers: [CurrencyPipe, DatePipe],
bootstrap: [AppComponent],

View File

@ -0,0 +1,45 @@
import { inject, Type } from "@angular/core";
import { Route, Router, Routes, UrlTree } from "@angular/router";
import { componentRouteSwap } from "@bitwarden/angular/utils/component-route-swap";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
/**
* Helper function to swap between two components based on the ExtensionRefresh feature flag.
* @param defaultComponent - The current non-refreshed component to render.
* @param refreshedComponent - The new refreshed component to render.
* @param options - The shared route options to apply to both components.
*/
export function extensionRefreshSwap(
defaultComponent: Type<any>,
refreshedComponent: Type<any>,
options: Route,
): Routes {
return componentRouteSwap(
defaultComponent,
refreshedComponent,
async () => {
const configService = inject(ConfigService);
return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
},
options,
);
}
/**
* Helper function to redirect to a new URL based on the ExtensionRefresh feature flag.
* @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled.
*/
export function extensionRefreshRedirect(redirectUrl: string): () => Promise<boolean | UrlTree> {
return async () => {
const configService = inject(ConfigService);
const router = inject(Router);
const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
if (shouldRedirect) {
return router.parseUrl(redirectUrl);
} else {
return true;
}
};
}

View File

@ -424,6 +424,10 @@ img,
.modal-title,
.overlay-container {
user-select: none;
&.user-select {
user-select: auto;
}
}
app-about .modal-body > *,

View File

@ -16,7 +16,7 @@ import {
CLIENT_TYPE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@ -59,7 +59,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
@ -210,6 +209,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: CryptoService,
useFactory: (
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService,
@ -223,6 +223,7 @@ const safeProviders: SafeProvider[] = [
kdfConfigService: KdfConfigService,
) => {
const cryptoService = new BrowserCryptoService(
pinService,
masterPasswordService,
keyGenerationService,
cryptoFunctionService,
@ -239,6 +240,7 @@ const safeProviders: SafeProvider[] = [
return cryptoService;
},
deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
KeyGenerationService,
CryptoFunctionService,
@ -411,7 +413,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE,
useFactory: (
regularMemoryStorageService: AbstractMemoryStorageService & ObservableStorageService,
regularMemoryStorageService: AbstractStorageService & ObservableStorageService,
) => {
if (BrowserApi.isManifestVersion(2)) {
return regularMemoryStorageService;
@ -439,7 +441,7 @@ const safeProviders: SafeProvider[] = [
useFactory: (
storageService: AbstractStorageService,
secureStorageService: AbstractStorageService,
memoryStorageService: AbstractMemoryStorageService,
memoryStorageService: AbstractStorageService,
logService: LogService,
accountService: AccountServiceAbstraction,
environmentService: EnvironmentService,

View File

@ -192,56 +192,5 @@
{{ "showIdentitiesCurrentTabDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
<input
id="favicon"
type="checkbox"
aria-describedby="faviconHelp"
(change)="updateFavicon()"
[(ngModel)]="enableFavicon"
/>
</div>
</div>
<div id="faviconHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
<input
id="badge"
type="checkbox"
aria-describedby="badgeHelp"
(change)="updateBadgeCounter()"
[(ngModel)]="enableBadgeCounter"
/>
</div>
</div>
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="theme">{{ "theme" | i18n }}</label>
<select
id="theme"
name="Theme"
aria-describedby="themeHelp"
[(ngModel)]="theme"
(change)="saveTheme()"
>
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
<div id="themeHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
</div>
</div>
</ng-container>
</main>

View File

@ -2,7 +2,6 @@ import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
@ -12,8 +11,6 @@ import {
} from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { enableAccountSwitching } from "../../platform/flags";
@ -23,8 +20,6 @@ import { enableAccountSwitching } from "../../platform/flags";
templateUrl: "options.component.html",
})
export class OptionsComponent implements OnInit {
enableFavicon = false;
enableBadgeCounter = true;
enableAutoFillOnPageLoad = false;
autoFillOnPageLoadDefault = false;
autoFillOnPageLoadOptions: any[];
@ -36,8 +31,6 @@ export class OptionsComponent implements OnInit {
showCardsCurrentTab = false;
showIdentitiesCurrentTab = false;
showClearClipboard = true;
theme: ThemeType;
themeOptions: any[];
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[];
clearClipboard: ClearClipboardDelaySetting;
@ -52,18 +45,9 @@ export class OptionsComponent implements OnInit {
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService,
private themeStateService: ThemeStateService,
private vaultSettingsService: VaultSettingsService,
) {
this.themeOptions = [
{ name: i18nService.t("default"), value: ThemeType.System },
{ name: i18nService.t("light"), value: ThemeType.Light },
{ name: i18nService.t("dark"), value: ThemeType.Dark },
{ name: "Nord", value: ThemeType.Nord },
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
];
this.uriMatchOptions = [
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
@ -117,14 +101,8 @@ export class OptionsComponent implements OnInit {
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
const defaultUriMatch = await firstValueFrom(
this.domainSettingsService.defaultUriMatchStrategy$,
);
@ -166,15 +144,6 @@ export class OptionsComponent implements OnInit {
await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault);
}
async updateFavicon() {
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
}
async updateBadgeCounter() {
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
this.messagingService.send("bgUpdateContextMenu");
}
async updateShowCardsCurrentTab() {
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
}
@ -183,10 +152,6 @@ export class OptionsComponent implements OnInit {
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
}
async saveTheme() {
await this.themeStateService.setSelectedTheme(this.theme);
}
async saveClearClipboard() {
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
}

View File

@ -1,264 +0,0 @@
<app-header>
<div class="left">
<app-pop-out></app-pop-out>
</div>
<h1 class="center">
<span class="title">{{ "settings" | i18n }}</span>
</h1>
<div class="right"></div>
</app-header>
<main tabindex="-1" [formGroup]="form">
<div class="box list">
<h2 class="box-header">{{ "manage" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/autofill"
>
<div class="row-main">{{ "autofill" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/folders"
>
<div class="row-main">{{ "folders" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/sync"
>
<div class="row-main">{{ "sync" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/excluded-domains"
>
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "security" | i18n }}</h2>
<div class="box-content single-line">
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
<span *ngIf="policy.timeout && policy.action">
{{
"vaultTimeoutPolicyWithActionInEffect"
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
}}
</span>
<span *ngIf="policy.timeout && !policy.action">
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
</span>
<span *ngIf="!policy.timeout && policy.action">
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
</span>
</app-callout>
<app-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</app-vault-timeout-input>
<div class="box-content-row display-block" appBoxRow>
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
<select
id="vaultTimeoutAction"
name="VaultTimeoutActions"
formControlName="vaultTimeoutAction"
>
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
{{ action | i18n }}
</option>
</select>
</div>
<div
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
id="unlockMethodHelp"
class="box-footer"
>
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
<input id="pin" type="checkbox" formControlName="pin" />
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
<input id="biometric" type="checkbox" formControlName="biometric" />
</div>
<div
class="box-content-row box-content-row-checkbox"
appBoxRow
*ngIf="supportsBiometric && this.form.value.biometric"
>
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
<input
id="autoBiometricsPrompt"
type="checkbox"
(change)="updateAutoBiometricsPrompt()"
formControlName="enableAutoBiometricsPrompt"
/>
</div>
<button
*ngIf="
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="lock()"
>
<div class="row-main">{{ "lockNow" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="twoStep()"
>
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "account" | i18n }}</h2>
<div class="box-content single-line">
<button type="button" class="box-content-row" routerLink="/premium">
<div class="row-main">
<div class="icon text-primary">
<i class="bwi bwi-fw bwi-lg bwi-star-f" aria-hidden="true"></i>
</div>
<span class="text text-primary"
><b>{{ "premiumMembership" | i18n }}</b></span
>
</div>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="changePassword()"
*ngIf="showChangeMasterPass"
>
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="fingerprint()"
>
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="!accountSwitcherEnabled"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="logOut()"
>
<div class="row-main">{{ "logOut" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "tools" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="import()"
>
<div class="row-main">{{ "importItems" | i18n }}</div>
<i
class="bwi bwi-external-link bwi-lg row-sub-icon bwi-rotate-270 bwi-fw"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="export()"
>
<div class="row-main">{{ "exportVault" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="webVault()"
>
<div class="row-main">{{ "bitWebVault" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "other" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/options"
>
<div class="row-main">{{ "options" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="about()"
>
<div class="row-main">{{ "about" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="share()"
>
<div class="row-main">{{ "learnOrg" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/help-and-feedback"
>
<div class="row-main">{{ "helpFeedback" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
aria-describedby="rateExtensionHelp"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="rate()"
>
<div class="row-main">{{ "rateExtension" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
<div id="rateExtensionHelp" class="box-footer">{{ "rateExtensionDesc" | i18n }}</div>
</div>
</main>

View File

@ -0,0 +1,11 @@
import { Component } from "@angular/core";
@Component({
selector: "app-tabs-v2",
template: `
<popup-tab-navigation>
<router-outlet></router-outlet>
</popup-tab-navigation>
`,
})
export class TabsV2Component {}

View File

@ -1,5 +1,9 @@
import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core";
import {
pinServiceFactory,
PinServiceInitOptions,
} from "../../../auth/background/service-factories/pin-service.factory";
import {
cryptoServiceFactory,
CryptoServiceInitOptions,
@ -36,7 +40,8 @@ export type ImportServiceInitOptions = ImportServiceFactoryOptions &
ImportApiServiceInitOptions &
I18nServiceInitOptions &
CollectionServiceInitOptions &
CryptoServiceInitOptions;
CryptoServiceInitOptions &
PinServiceInitOptions;
export function importServiceFactory(
cache: {
@ -56,6 +61,7 @@ export function importServiceFactory(
await i18nServiceFactory(cache, opts),
await collectionServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts),
await pinServiceFactory(cache, opts),
),
);
}

View File

@ -5,7 +5,7 @@
<div bitDialogTitle>Bitwarden</div>
<div bitDialogContent>
<p>&copy; Bitwarden Inc. 2015-{{ year }}</p>
<p>{{ "version" | i18n }}: {{ version$ | async }}</p>
<p class="user-select">{{ "version" | i18n }}: {{ version$ | async }}</p>
<ng-container *ngIf="data$ | async as data">
<p *ngIf="data.isCloud">
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
@ -16,7 +16,7 @@
<ng-container *ngIf="!data.isCloud">
<ng-container *ngIf="data.serverConfig.server">
<p>
<p class="user-select">
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
{{ data.serverConfig?.version }}
<span *ngIf="!data.serverConfig.isValid()">
@ -28,7 +28,7 @@
</div>
</ng-container>
<p *ngIf="!data.serverConfig.server">
<p class="user-select" *ngIf="!data.serverConfig.server">
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
{{ data.serverConfig?.version }}
<span *ngIf="!data.serverConfig.isValid()">

View File

@ -1,7 +1,7 @@
<form (ngSubmit)="submit()" [formGroup]="exportForm">
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<button type="button" routerLink="/vault-settings">
<span class="header-icon" aria-hidden="true"><i class="bwi bwi-angle-left"></i></span>
<span>{{ "back" | i18n }}</span>
</button>

View File

@ -1,6 +1,6 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<button type="button" routerLink="/vault-settings">
<span class="header-icon" aria-hidden="true"><i class="bwi bwi-angle-left"></i></span>
<span>{{ "back" | i18n }}</span>
</button>

View File

@ -0,0 +1,136 @@
<app-header>
<div class="left">
<app-pop-out></app-pop-out>
</div>
<h1 class="center">
<span class="title">{{ "settings" | i18n }}</span>
</h1>
<div class="right"></div>
</app-header>
<main tabindex="-1">
<div class="box list">
<h2 class="box-header">{{ "manage" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/account-security"
>
<div class="row-main">{{ "accountSecurity" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/autofill"
>
<div class="row-main">{{ "autofill" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/notifications"
>
<div class="row-main">{{ "notifications" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/vault-settings"
>
<div class="row-main">{{ "vault" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "account" | i18n }}</h2>
<div class="box-content single-line">
<button type="button" class="box-content-row" routerLink="/premium">
<div class="row-main">
<div class="icon text-primary">
<i class="bwi bwi-fw bwi-lg bwi-star-f" aria-hidden="true"></i>
</div>
<span class="text text-primary"
><b>{{ "premiumMembership" | i18n }}</b></span
>
</div>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
</button>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "tools" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="webVault()"
>
<div class="row-main">{{ "bitWebVault" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "other" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/options"
>
<div class="row-main">{{ "options" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/appearance"
>
<div class="row-main">{{ "appearance" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="about()"
>
<div class="row-main">{{ "about" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="share()"
>
<div class="row-main">{{ "learnOrg" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/help-and-feedback"
>
<div class="row-main">{{ "helpFeedback" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
aria-describedby="rateExtensionHelp"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="rate()"
>
<div class="row-main">{{ "rateExtension" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
<div id="rateExtensionHelp" class="box-footer">{{ "rateExtensionDesc" | i18n }}</div>
</div>
</main>

View File

@ -0,0 +1,101 @@
import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, Subject } from "rxjs";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { DeviceType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { AboutComponent } from "./about/about.component";
const RateUrls = {
[DeviceType.ChromeExtension]:
"https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
[DeviceType.FirefoxExtension]:
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews",
[DeviceType.OperaExtension]:
"https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container",
[DeviceType.EdgeExtension]:
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
[DeviceType.VivaldiExtension]:
"https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews",
[DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147",
};
@Component({
selector: "tools-settings",
templateUrl: "settings.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SettingsComponent implements OnInit {
private destroy$ = new Subject<void>();
constructor(
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private vaultTimeoutService: VaultTimeoutService,
public messagingService: MessagingService,
private router: Router,
private environmentService: EnvironmentService,
private dialogService: DialogService,
) {}
async ngOnInit() {}
async share() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "learnOrg" },
content: { key: "learnOrgConfirmation" },
type: "info",
});
if (confirmed) {
// 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
BrowserApi.createNewTab("https://bitwarden.com/help/about-organizations/");
}
}
async webVault() {
const env = await firstValueFrom(this.environmentService.environment$);
const url = env.getWebVaultUrl();
await BrowserApi.createNewTab(url);
}
async import() {
await this.router.navigate(["/import"]);
if (await BrowserApi.isPopupOpen()) {
// 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
BrowserPopupUtils.openCurrentPagePopout(window);
}
}
export() {
// 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.router.navigate(["/export"]);
}
about() {
this.dialogService.open(AboutComponent);
}
rate() {
const deviceType = this.platformUtilsService.getDevice();
// 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
BrowserApi.createNewTab((RateUrls as any)[deviceType]);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -5,6 +5,7 @@ import { first } from "rxjs/operators";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -26,6 +27,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
private route: ActivatedRoute,
private location: Location,
logService: LogService,
configService: ConfigService,
) {
super(
collectionService,
@ -34,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
cipherService,
organizationService,
logService,
configService,
);
}

View File

@ -0,0 +1 @@
<h1>Vault V2 Extension Refresh</h1>

View File

@ -0,0 +1,13 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
@Component({
selector: "app-vault",
templateUrl: "vault-v2.component.html",
})
export class VaultV2Component implements OnInit, OnDestroy {
constructor() {}
ngOnInit(): void {}
ngOnDestroy(): void {}
}

View File

@ -0,0 +1,67 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "appearance" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="theme">{{ "theme" | i18n }}</label>
<select
id="theme"
name="Theme"
aria-describedby="themeHelp"
[(ngModel)]="theme"
(change)="saveTheme()"
>
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
<div id="themeHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
<input
id="badge"
type="checkbox"
aria-describedby="badgeHelp"
(change)="updateBadgeCounter()"
[(ngModel)]="enableBadgeCounter"
/>
</div>
</div>
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
<input
id="favicon"
type="checkbox"
aria-describedby="faviconHelp"
(change)="updateFavicon()"
[(ngModel)]="enableFavicon"
/>
</div>
</div>
<div id="faviconHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
</div>
</div>
</main>

View File

@ -0,0 +1,62 @@
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { enableAccountSwitching } from "../../../platform/flags";
@Component({
selector: "vault-appearance",
templateUrl: "appearance.component.html",
})
export class AppearanceComponent implements OnInit {
enableFavicon = false;
enableBadgeCounter = true;
theme: ThemeType;
themeOptions: any[];
accountSwitcherEnabled = false;
constructor(
private messagingService: MessagingService,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService,
private themeStateService: ThemeStateService,
) {
this.themeOptions = [
{ name: i18nService.t("default"), value: ThemeType.System },
{ name: i18nService.t("light"), value: ThemeType.Light },
{ name: i18nService.t("dark"), value: ThemeType.Dark },
{ name: "Nord", value: ThemeType.Nord },
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
];
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
}
async updateFavicon() {
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
}
async updateBadgeCounter() {
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
this.messagingService.send("bgUpdateContextMenu");
}
async saveTheme() {
await this.themeStateService.setSelectedTheme(this.theme);
}
}

View File

@ -1,6 +1,6 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<button type="button" routerLink="/vault-settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>

View File

@ -1,6 +1,6 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<button type="button" routerLink="/vault-settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>

View File

@ -0,0 +1,56 @@
<app-header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "vault" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</app-header>
<main tabindex="-1">
<div class="box list">
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/folders"
>
<div class="row-main">{{ "folders" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="import()"
>
<div class="row-main">{{ "importItems" | i18n }}</div>
<i
class="bwi bwi-external-link bwi-lg row-sub-icon bwi-rotate-270 bwi-fw"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/export"
>
<div class="row-main">{{ "exportVault" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/sync"
>
<div class="row-main">{{ "sync" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
@Component({
selector: "vault-settings",
templateUrl: "vault-settings.component.html",
})
export class VaultSettingsComponent {
constructor(
public messagingService: MessagingService,
private router: Router,
) {}
async import() {
await this.router.navigate(["/import"]);
if (await BrowserApi.isPopupOpen()) {
await BrowserPopupUtils.openCurrentPagePopout(window);
}
}
}

View File

@ -124,7 +124,7 @@
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
</data>
<data name="Description" xml:space="preserve">
<value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
<value>Recognised as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
SECURE YOUR DIGITAL LIFE
Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access.
@ -146,7 +146,7 @@ More reasons to choose Bitwarden:
World-Class Encryption
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private.
3rd-party Audits
3rd-Party Audits
Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications.
Advanced 2FA
@ -159,13 +159,13 @@ Built-in Generator
Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy.
Global Translations
Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin.
Bitwarden translations exist for more than 60 languages, translated by the global community through Crowdin.
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, desktop OS, and more.
Bitwarden secures more than just passwords
End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
End-to-end encrypted credential management solutions from Bitwarden empower organisations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
</value>
</data>
<data name="AssetTitle" xml:space="preserve">

View File

@ -133,7 +133,7 @@ ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE
Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions.
EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE
Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features.
Utilise Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features.
EMPOWER YOUR TEAMS WITH BITWARDEN
Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more.
@ -165,7 +165,7 @@ Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Bitwarden secures more than just passwords
End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
End-to-end encrypted credential management solutions from Bitwarden empower organisations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
</value>
</data>
<data name="AssetTitle" xml:space="preserve">

View File

@ -118,10 +118,10 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Bitwarden Password Manager</value>
<value>Bitwarden - Administrador de contraseñas</value>
</data>
<data name="Summary" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
<value>En casa, en el trabajo o en el viaje, Bitwarden asegura fácilmente todas sus contraseñas, claves de acceso e información confidencial.</value>
</data>
<data name="Description" xml:space="preserve">
<value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga
</value>
</data>
<data name="AssetTitle" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
<value>En casa, en el trabajo o mientras viaja, Bitwarden protege fácilmente todas sus contraseñas, claves de acceso e información confidencial.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>Sincroniza y accede a tu caja fuerte desde múltiples dispositivos</value>

View File

@ -165,7 +165,7 @@ Aplicações multiplataforma
Proteja e partilhe dados confidenciais dentro do seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel, ou SO de computador, e muito mais.
O Bitwarden protege mais do que apenas palavras-passe
As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden permitem que as organizações protejam tudo, incluindo segredos de programadores e experiências com chaves de acesso. Visite Bitwarden.com para saber mais sobre o Gestor de Segredos do Bitwarden e o Bitwarden Passwordless.dev!
As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden permitem que as organizações protejam tudo, incluindo segredos de programadores e experiências com chaves de acesso. Visite Bitwarden.com para saber mais sobre o Bitwarden - Gestor de Segredos e o Bitwarden Passwordless.dev!
</value>
</data>
<data name="AssetTitle" xml:space="preserve">

View File

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

View File

@ -86,7 +86,7 @@ export class UnlockCommand {
if (passwordValid) {
await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey);
if (await this.keyConnectorService.getConvertAccountRequired()) {

View File

@ -10,8 +10,8 @@ import {
AuthRequestService,
LoginStrategyService,
LoginStrategyServiceAbstraction,
PinCryptoService,
PinCryptoServiceAbstraction,
PinService,
PinServiceAbstraction,
UserDecryptionOptionsService,
} from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
@ -208,7 +208,7 @@ export class Main {
cipherFileUploadService: CipherFileUploadService;
keyConnectorService: KeyConnectorService;
userVerificationService: UserVerificationService;
pinCryptoService: PinCryptoServiceAbstraction;
pinService: PinServiceAbstraction;
stateService: StateService;
autofillSettingsService: AutofillSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService;
@ -360,11 +360,29 @@ export class Main {
migrationRunner,
);
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
this.keyGenerationService,
this.encryptService,
);
this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.pinService = new PinService(
this.accountService,
this.cryptoFunctionService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.masterPasswordService,
this.stateProvider,
this.stateService,
);
this.cryptoService = new CryptoService(
this.pinService,
this.masterPasswordService,
this.keyGenerationService,
this.cryptoFunctionService,
@ -486,6 +504,7 @@ export class Main {
this.stateProvider,
this.secureStorageService,
this.userDecryptionOptionsService,
this.logService,
);
this.authRequestService = new AuthRequestService(
@ -574,6 +593,8 @@ export class Main {
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.accountService,
this.pinService,
this.userDecryptionOptionsService,
this.cryptoService,
this.tokenService,
@ -582,14 +603,6 @@ export class Main {
this.biometricStateService,
);
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
this.kdfConfigService,
);
this.userVerificationService = new UserVerificationService(
this.stateService,
this.cryptoService,
@ -598,7 +611,7 @@ export class Main {
this.i18nService,
this.userVerificationApiService,
this.userDecryptionOptionsService,
this.pinCryptoService,
this.pinService,
this.logService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
@ -661,11 +674,13 @@ export class Main {
this.i18nService,
this.collectionService,
this.cryptoService,
this.pinService,
);
this.individualExportService = new IndividualVaultExportService(
this.folderService,
this.cipherService,
this.pinService,
this.cryptoService,
this.cryptoFunctionService,
this.kdfConfigService,
@ -674,6 +689,7 @@ export class Main {
this.organizationExportService = new OrganizationVaultExportService(
this.cipherService,
this.apiService,
this.pinService,
this.cryptoService,
this.cryptoFunctionService,
this.collectionService,
@ -685,6 +701,8 @@ export class Main {
this.organizationExportService,
);
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
this.program = new Program(this);
this.vaultProgram = new VaultProgram(this);
@ -708,8 +726,6 @@ export class Main {
);
this.providerApiService = new ProviderApiService(this.apiService);
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
}
async run() {

View File

@ -1,5 +1,6 @@
import * as chalk from "chalk";
import { program, Command, OptionValues } from "commander";
import { firstValueFrom } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@ -63,8 +64,16 @@ export class Program {
process.env.BW_NOINTERACTION = "true";
});
program.on("option:session", (key) => {
program.on("option:session", async (key) => {
process.env.BW_SESSION = key;
// once we have the session key, we can set the user key in memory
const activeAccount = await firstValueFrom(this.main.accountService.activeAccount$);
if (activeAccount) {
await this.main.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(
activeAccount.id,
);
}
});
program.on("command:*", () => {

View File

@ -1,5 +1,5 @@
{
"dev_flags": {},
"devFlags": {},
"flags": {
"multithreadDecryption": false,
"enableCipherKeyEncryption": false

View File

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

View File

@ -1,12 +1,13 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs";
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
@ -19,7 +20,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid";
@ -111,6 +112,7 @@ export class SettingsComponent implements OnInit {
private destroy$ = new Subject<void>();
constructor(
private accountService: AccountService,
private policyService: PolicyService,
private formBuilder: FormBuilder,
private i18nService: I18nService,
@ -127,6 +129,7 @@ export class SettingsComponent implements OnInit {
private desktopSettingsService: DesktopSettingsService,
private biometricStateService: BiometricStateService,
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
private pinService: PinServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
private logService: LogService,
private nativeMessagingManifestService: NativeMessagingManifestService,
@ -243,9 +246,10 @@ export class SettingsComponent implements OnInit {
}),
);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
// Load initial values
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
this.userHasPinSet = pinStatus !== "DISABLED";
this.userHasPinSet = await this.pinService.isPinSet(userId);
const initialValues = {
vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(),

View File

@ -8,7 +8,7 @@ import {
ViewContainerRef,
} from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, map, Subject, takeUntil } from "rxjs";
import { filter, firstValueFrom, map, Subject, takeUntil, timeout } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@ -218,8 +218,10 @@ export class AppComponent implements OnInit, OnDestroy {
await this.vaultTimeoutService.lock(message.userId);
break;
case "lockAllVaults": {
const currentUser = await this.stateService.getUserId();
const accounts = await firstValueFrom(this.stateService.accounts$);
const currentUser = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a.id)),
);
const accounts = await firstValueFrom(this.accountService.accounts$);
await this.vaultTimeoutService.lock(currentUser);
for (const account of Object.keys(accounts)) {
if (account === currentUser) {
@ -564,19 +566,42 @@ export class AppComponent implements OnInit, OnDestroy {
this.messagingService.send("updateAppMenu", { updateRequest: updateRequest });
}
private async logOut(expired: boolean, userId?: string) {
const userBeingLoggedOut =
(userId as UserId) ??
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
// Even though the userId parameter is no longer optional doesn't mean a message couldn't be
// passing null-ish values to us.
private async logOut(expired: boolean, userId: UserId) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const userBeingLoggedOut = userId ?? activeUserId;
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
// doesn't attempt to update a user that is being logged out as we will manually
// call updateAppMenu when the logout is complete.
this.startAccountCleanUp(userBeingLoggedOut);
let preLogoutActiveUserId;
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
const nextUpAccount =
activeUserId === userBeingLoggedOut
? await firstValueFrom(this.accountService.nextUpAccount$) // We'll need to switch accounts
: null;
try {
// HACK: We shouldn't wait for authentication status to change here but instead subscribe to the
// authentication status to do various actions.
const logoutPromise = firstValueFrom(
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
timeout({
first: 5_000,
with: () => {
throw new Error(
"The logout process did not complete in a reasonable amount of time.",
);
},
}),
),
);
// Provide the userId of the user to upload events for
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
@ -590,26 +615,33 @@ export class AppComponent implements OnInit, OnDestroy {
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
preLogoutActiveUserId = this.activeUserId;
await this.stateService.clean({ userId: userBeingLoggedOut });
await this.accountService.clean(userBeingLoggedOut);
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
await logoutPromise;
} finally {
this.finishAccountCleanUp(userBeingLoggedOut);
}
if (nextUpAccount == null) {
// 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.router.navigate(["login"]);
} else if (preLogoutActiveUserId !== nextUpAccount.id) {
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
// We only need to change the display at all if the account being looked at is the one
// being logged out. If it was a background account, no need to do anything.
if (userBeingLoggedOut === activeUserId) {
if (nextUpAccount != null) {
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
} else {
// We don't have another user to switch to, bring them to the login page so they
// can sign into a user.
await this.accountService.switchAccount(null);
void this.router.navigate(["login"]);
}
}
await this.updateAppMenu();
// This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up
if (userBeingLoggedOut === preLogoutActiveUserId) {
if (userBeingLoggedOut === activeUserId) {
this.authService.logOut(async () => {
if (expired) {
this.platformUtilsService.showToast(
@ -690,7 +722,7 @@ export class AppComponent implements OnInit, OnDestroy {
}
private async checkForSystemTimeout(timeout: number): Promise<void> {
const accounts = await firstValueFrom(this.stateService.accounts$);
const accounts = await firstValueFrom(this.accountService.accounts$);
for (const userId in accounts) {
if (userId == null) {
continue;
@ -700,7 +732,7 @@ export class AppComponent implements OnInit, OnDestroy {
// 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
options[1] === "logOut"
? this.logOut(false, userId)
? this.logOut(false, userId as UserId)
: await this.vaultTimeoutService.lock(userId);
}
}

View File

@ -92,6 +92,11 @@ export class AccountSwitcherComponent {
return null;
}
if (!active.name && !active.email) {
// We need to have this information at minimum to display them.
return null;
}
return {
id: active.id,
name: active.name,

View File

@ -59,6 +59,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService } from "@bitwarden/components";
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { Account } from "../../models/account";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@ -183,6 +184,7 @@ const safeProviders: SafeProvider[] = [
provide: SystemServiceAbstraction,
useClass: SystemService,
deps: [
PinServiceAbstraction,
MessagingServiceAbstraction,
PlatformUtilsServiceAbstraction,
RELOAD_CALLBACK,
@ -221,7 +223,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: EncryptedMessageHandlerService,
deps: [
StateServiceAbstraction,
AccountServiceAbstraction,
AuthServiceAbstraction,
CipherServiceAbstraction,
PolicyServiceAbstraction,
@ -250,6 +252,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService,
deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction,

View File

@ -12,8 +12,16 @@
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart">
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" />
<label
class="tw-flex tw-items-start tw-gap-2"
*ngIf="showMasterPasswordOnClientRestartOption"
>
<input
class="tw-mt-1"
type="checkbox"
bitCheckbox
formControlName="requireMasterPasswordOnClientRestart"
/>
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
</label>
</div>

View File

@ -6,7 +6,7 @@ import { of } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
import { PinServiceAbstraction } 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";
@ -155,8 +155,8 @@ describe("LockComponent", () => {
useValue: mock<UserVerificationService>(),
},
{
provide: PinCryptoServiceAbstraction,
useValue: mock<PinCryptoServiceAbstraction>(),
provide: PinServiceAbstraction,
useValue: mock<PinServiceAbstraction>(),
},
{
provide: BiometricStateService,

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