diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains.component.html b/apps/browser/src/autofill/popup/settings/excluded-domains.component.html
index 1dec438fdd2..e3b6bf5f802 100644
--- a/apps/browser/src/autofill/popup/settings/excluded-domains.component.html
+++ b/apps/browser/src/autofill/popup/settings/excluded-domains.component.html
@@ -11,7 +11,7 @@
accountSwitcherEnabled ? ("excludedDomainsDescAlt" | i18n) : ("excludedDomainsDesc" | i18n)
}}
-
+
{{ "domainsTitle" | i18n }}
{{ excludedDomainsState?.length || 0 }}
@@ -57,7 +57,13 @@
-
diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts b/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts
index 381ba903423..f622312ce04 100644
--- a/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts
+++ b/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts
@@ -1,13 +1,19 @@
import { CommonModule } from "@angular/common";
-import { QueryList, Component, ElementRef, OnDestroy, OnInit, ViewChildren } from "@angular/core";
+import {
+ QueryList,
+ Component,
+ ElementRef,
+ OnDestroy,
+ AfterViewInit,
+ ViewChildren,
+} from "@angular/core";
import { FormsModule } from "@angular/forms";
-import { Router, RouterModule } from "@angular/router";
-import { firstValueFrom, Subject, takeUntil } from "rxjs";
+import { RouterModule } from "@angular/router";
+import { Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
-import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -29,8 +35,6 @@ import { PopupFooterComponent } from "../../../platform/popup/layout/popup-foote
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
-const BroadcasterSubscriptionId = "excludedDomainsState";
-
@Component({
selector: "app-excluded-domains",
templateUrl: "excluded-domains.component.html",
@@ -55,11 +59,12 @@ const BroadcasterSubscriptionId = "excludedDomainsState";
TypographyModule,
],
})
-export class ExcludedDomainsComponent implements OnInit, OnDestroy {
+export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy {
@ViewChildren("uriInput") uriInputElements: QueryList>;
accountSwitcherEnabled = false;
dataIsPristine = true;
+ isLoading = false;
excludedDomainsState: string[] = [];
storedExcludedDomains: string[] = [];
// How many fields should be non-editable before editable fields
@@ -70,16 +75,27 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
constructor(
private domainSettingsService: DomainSettingsService,
private i18nService: I18nService,
- private router: Router,
- private broadcasterService: BroadcasterService,
private platformUtilsService: PlatformUtilsService,
) {
this.accountSwitcherEnabled = enableAccountSwitching();
}
- async ngOnInit() {
- const neverDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
+ async ngAfterViewInit() {
+ this.domainSettingsService.neverDomains$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((neverDomains: NeverDomains) => this.handleStateUpdate(neverDomains));
+ this.uriInputElements.changes.pipe(takeUntil(this.destroy$)).subscribe(({ last }) => {
+ this.focusNewUriInput(last);
+ });
+ }
+
+ ngOnDestroy() {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ handleStateUpdate(neverDomains: NeverDomains) {
if (neverDomains) {
this.storedExcludedDomains = Object.keys(neverDomains);
}
@@ -89,15 +105,8 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
// Do not allow the first x (pre-existing) fields to be edited
this.fieldsEditThreshold = this.storedExcludedDomains.length;
- this.uriInputElements.changes.pipe(takeUntil(this.destroy$)).subscribe(({ last }) => {
- this.focusNewUriInput(last);
- });
- }
-
- ngOnDestroy() {
- this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
- this.destroy$.next();
- this.destroy$.complete();
+ this.dataIsPristine = true;
+ this.isLoading = false;
}
focusNewUriInput(elementRef: ElementRef) {
@@ -116,7 +125,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
async removeDomain(i: number) {
this.excludedDomainsState.splice(i, 1);
- // if a pre-existing field was dropped, lower the edit threshold
+ // If a pre-existing field was dropped, lower the edit threshold
if (i < this.fieldsEditThreshold) {
this.fieldsEditThreshold--;
}
@@ -132,11 +141,11 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
async saveChanges() {
if (this.dataIsPristine) {
- await this.router.navigate(["/notifications"]);
-
return;
}
+ this.isLoading = true;
+
const newExcludedDomainsSaveState: NeverDomains = {};
const uniqueExcludedDomains = new Set(this.excludedDomainsState);
@@ -151,6 +160,8 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
this.i18nService.t("excludedDomainsInvalidDomain", uri),
);
+ // Don't reset via `handleStateUpdate` to allow existing input value correction
+ this.isLoading = false;
return;
}
@@ -159,7 +170,23 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
}
try {
- await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState);
+ const existingState = new Set(this.storedExcludedDomains);
+ const newState = new Set(Object.keys(newExcludedDomainsSaveState));
+ const stateIsUnchanged =
+ existingState.size === newState.size &&
+ new Set([...existingState, ...newState]).size === existingState.size;
+
+ // The subscriber updates don't trigger if `setNeverDomains` sets an equivalent state
+ if (stateIsUnchanged) {
+ // Reset UI state directly
+ const constructedNeverDomainsState = this.storedExcludedDomains.reduce(
+ (neverDomains, uri) => ({ ...neverDomains, [uri]: null }),
+ {},
+ );
+ this.handleStateUpdate(constructedNeverDomainsState);
+ } else {
+ await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState);
+ }
this.platformUtilsService.showToast(
"success",
@@ -169,11 +196,9 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} catch {
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
- // Do not navigate on error
- return;
+ // Don't reset via `handleStateUpdate` to preserve input values
+ this.isLoading = false;
}
-
- await this.router.navigate(["/notifications"]);
}
trackByFunction(index: number, _: string) {
diff --git a/apps/browser/src/vault/popup/components/vault/view.component.html b/apps/browser/src/vault/popup/components/vault/view.component.html
index ff76c68464d..73415c9070a 100644
--- a/apps/browser/src/vault/popup/components/vault/view.component.html
+++ b/apps/browser/src/vault/popup/components/vault/view.component.html
@@ -472,7 +472,7 @@
attr.aria-label="{{ 'launch' | i18n }} {{ u.uri }}"
appA11yTitle="{{ 'launch' | i18n }}"
*ngIf="u.canLaunch"
- (click)="launch(u)"
+ (click)="launch(u, cipher.id)"
>
diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts
index 4c96c10dac3..2ff34ebafa5 100644
--- a/libs/angular/src/vault/components/view.component.ts
+++ b/libs/angular/src/vault/components/view.component.ts
@@ -348,15 +348,13 @@ export class ViewComponent implements OnDestroy, OnInit {
}
}
- launch(uri: Launchable, cipherId?: string) {
+ async launch(uri: Launchable, cipherId?: string) {
if (!uri.canLaunch) {
return;
}
if (cipherId) {
- // 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.cipherService.updateLastLaunchedDate(cipherId);
+ await this.cipherService.updateLastLaunchedDate(cipherId);
}
this.platformUtilsService.launchUri(uri.launchUri);
diff --git a/libs/common/src/platform/enums/encryption-type.enum.ts b/libs/common/src/platform/enums/encryption-type.enum.ts
index b4ecd780499..a0ffe679279 100644
--- a/libs/common/src/platform/enums/encryption-type.enum.ts
+++ b/libs/common/src/platform/enums/encryption-type.enum.ts
@@ -8,6 +8,14 @@ export enum EncryptionType {
Rsa2048_OaepSha1_HmacSha256_B64 = 6,
}
+export function encryptionTypeToString(encryptionType: EncryptionType): string {
+ if (encryptionType in EncryptionType) {
+ return EncryptionType[encryptionType];
+ } else {
+ return "Unknown encryption type " + encryptionType;
+ }
+}
+
/** The expected number of parts to a serialized EncString of the given encryption type.
* For example, an EncString of type AesCbc256_B64 will have 2 parts, and an EncString of type
* AesCbc128_HmacSha256_B64 will have 3 parts.
diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts
index 681972e7e4b..8c42c724b24 100644
--- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts
+++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts
@@ -2,7 +2,7 @@ import { Utils } from "../../../platform/misc/utils";
import { CryptoFunctionService } from "../../abstractions/crypto-function.service";
import { EncryptService } from "../../abstractions/encrypt.service";
import { LogService } from "../../abstractions/log.service";
-import { EncryptionType } from "../../enums";
+import { EncryptionType, encryptionTypeToString as encryptionTypeName } from "../../enums";
import { Decryptable } from "../../interfaces/decryptable.interface";
import { Encrypted } from "../../interfaces/encrypted";
import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface";
@@ -70,13 +70,24 @@ export class EncryptServiceImplementation implements EncryptService {
key = this.resolveLegacyKey(key, encString);
+ // DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
if (key.macKey != null && encString?.mac == null) {
- this.logService.error("MAC required but not provided.");
+ this.logService.error(
+ "[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
+ encryptionTypeName(key.encType) +
+ " Payload type " +
+ encryptionTypeName(encString.encryptionType),
+ );
return null;
}
if (key.encType !== encString.encryptionType) {
- this.logService.error("Key encryption type does not match payload encryption type.");
+ this.logService.error(
+ "[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
+ encryptionTypeName(key.encType) +
+ " Payload type " +
+ encryptionTypeName(encString.encryptionType),
+ );
return null;
}
@@ -94,7 +105,12 @@ export class EncryptServiceImplementation implements EncryptService {
);
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
if (!macsEqual) {
- this.logMacFailed("MAC comparison failed. Key or payload has changed.");
+ this.logMacFailed(
+ "[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " +
+ encryptionTypeName(key.encType) +
+ " Payload type " +
+ encryptionTypeName(encString.encryptionType),
+ );
return null;
}
}
@@ -113,13 +129,24 @@ export class EncryptServiceImplementation implements EncryptService {
key = this.resolveLegacyKey(key, encThing);
+ // DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
if (key.macKey != null && encThing.macBytes == null) {
- this.logService.error("MAC required but not provided.");
+ this.logService.error(
+ "[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
+ encryptionTypeName(key.encType) +
+ " Payload type " +
+ encryptionTypeName(encThing.encryptionType),
+ );
return null;
}
if (key.encType !== encThing.encryptionType) {
- this.logService.error("Key encryption type does not match payload encryption type.");
+ this.logService.error(
+ "[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
+ encryptionTypeName(key.encType) +
+ " Payload type " +
+ encryptionTypeName(encThing.encryptionType),
+ );
return null;
}
@@ -129,13 +156,25 @@ export class EncryptServiceImplementation implements EncryptService {
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
if (computedMac === null) {
- this.logMacFailed("Failed to compute MAC.");
+ this.logMacFailed(
+ "[Encrypt service] Failed to compute MAC." +
+ " Key type " +
+ encryptionTypeName(key.encType) +
+ " Payload type " +
+ encryptionTypeName(encThing.encryptionType),
+ );
return null;
}
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
- this.logMacFailed("MAC comparison failed. Key or payload has changed.");
+ this.logMacFailed(
+ "[Encrypt service] MAC comparison failed. Key or payload has changed." +
+ " Key type " +
+ encryptionTypeName(key.encType) +
+ " Payload type " +
+ encryptionTypeName(encThing.encryptionType),
+ );
return null;
}
}
@@ -164,7 +203,7 @@ export class EncryptServiceImplementation implements EncryptService {
async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise {
if (data == null) {
- throw new Error("No data provided for decryption.");
+ throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption.");
}
let algorithm: "sha1" | "sha256";
@@ -182,7 +221,7 @@ export class EncryptServiceImplementation implements EncryptService {
}
if (privateKey == null) {
- throw new Error("No private key provided for decryption.");
+ throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption.");
}
return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm);
diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts
index a7377a93eec..207a5da3cbf 100644
--- a/libs/common/src/vault/services/cipher.service.ts
+++ b/libs/common/src/vault/services/cipher.service.ts
@@ -650,14 +650,11 @@ export class CipherService implements CipherServiceAbstraction {
ciphersLocalData = {};
}
- const cipherId = id as CipherId;
- if (ciphersLocalData[cipherId]) {
- ciphersLocalData[cipherId].lastLaunched = new Date().getTime();
- } else {
- ciphersLocalData[cipherId] = {
- lastUsedDate: new Date().getTime(),
- };
- }
+ const currentTime = new Date().getTime();
+ ciphersLocalData[id as CipherId] = {
+ lastLaunched: currentTime,
+ lastUsedDate: currentTime,
+ };
await this.localDataState.update(() => ciphersLocalData);
diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html
index 06ea1f767b7..737e32fa1f9 100644
--- a/libs/tools/generator/components/src/credential-generator.component.html
+++ b/libs/tools/generator/components/src/credential-generator.component.html
@@ -56,7 +56,12 @@