1
0
mirror of https://github.com/bitwarden/browser synced 2026-03-02 03:21:19 +00:00

Merge branch 'main' into ps/extension-refresh

This commit is contained in:
Victoria League
2024-10-24 10:27:23 -04:00
committed by GitHub
17 changed files with 226 additions and 70 deletions

View File

@@ -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);

View File

@@ -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.

View File

@@ -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<Uint8Array> {
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);

View File

@@ -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);

View File

@@ -56,7 +56,12 @@
<form [formGroup]="username" class="box tw-container">
<bit-form-field>
<bit-label>{{ "type" | i18n }}</bit-label>
<bit-select [items]="usernameOptions$ | async" formControlName="nav"> </bit-select>
<bit-select
[items]="usernameOptions$ | async"
formControlName="nav"
data-testid="username-type"
>
</bit-select>
<bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{
credentialTypeHint$ | async
}}</bit-hint>
@@ -65,7 +70,12 @@
<form *ngIf="showForwarder$ | async" [formGroup]="forwarder" class="box tw-container">
<bit-form-field>
<bit-label>{{ "service" | i18n }}</bit-label>
<bit-select [items]="forwarderOptions$ | async" formControlName="nav"> </bit-select>
<bit-select
[items]="forwarderOptions$ | async"
formControlName="nav"
data-testid="email-forwarding-service"
>
</bit-select>
</bit-form-field>
</form>
<tools-catchall-settings

View File

@@ -37,6 +37,7 @@ import {
isUsernameAlgorithm,
toCredentialGeneratorConfiguration,
} from "@bitwarden/generator-core";
import { GeneratorHistoryService } from "@bitwarden/generator-history";
// constants used to identify navigation selections that are not
// generator algorithms
@@ -51,6 +52,7 @@ const NONE_SELECTED = "none";
export class CredentialGeneratorComponent implements OnInit, OnDestroy {
constructor(
private generatorService: CredentialGeneratorService,
private generatorHistoryService: GeneratorHistoryService,
private toastService: ToastService,
private logService: LogService,
private i18nService: I18nService,
@@ -182,9 +184,16 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
// continue with origin stream
return generator;
}),
withLatestFrom(this.userId$),
takeUntil(this.destroyed),
)
.subscribe((generated) => {
.subscribe(([generated, userId]) => {
this.generatorHistoryService
.track(userId, generated.credential, generated.category, generated.generationDate)
.catch((e: unknown) => {
this.logService.error(e);
});
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {

View File

@@ -2,6 +2,7 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
import {
BehaviorSubject,
catchError,
distinctUntilChanged,
filter,
map,
@@ -14,7 +15,9 @@ import {
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { Option } from "@bitwarden/components/src/select/option";
import {
CredentialGeneratorService,
@@ -25,6 +28,7 @@ import {
isPasswordAlgorithm,
AlgorithmInfo,
} from "@bitwarden/generator-core";
import { GeneratorHistoryService } from "@bitwarden/generator-history";
/** Options group for passwords */
@Component({
@@ -34,6 +38,9 @@ import {
export class PasswordGeneratorComponent implements OnInit, OnDestroy {
constructor(
private generatorService: CredentialGeneratorService,
private generatorHistoryService: GeneratorHistoryService,
private toastService: ToastService,
private logService: LogService,
private i18nService: I18nService,
private accountService: AccountService,
private zone: NgZone,
@@ -109,10 +116,32 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
// wire up the generator
this.algorithm$
.pipe(
filter((algorithm) => !!algorithm),
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
catchError((error: unknown, generator) => {
if (typeof error === "string") {
this.toastService.showToast({
message: error,
variant: "error",
title: "",
});
} else {
this.logService.error(error);
}
// continue with origin stream
return generator;
}),
withLatestFrom(this.userId$),
takeUntil(this.destroyed),
)
.subscribe((generated) => {
.subscribe(([generated, userId]) => {
this.generatorHistoryService
.track(userId, generated.credential, generated.category, generated.generationDate)
.catch((e: unknown) => {
this.logService.error(e);
});
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {

View File

@@ -33,7 +33,12 @@
<form class="box" [formGroup]="username" class="tw-container">
<bit-form-field>
<bit-label>{{ "type" | i18n }}</bit-label>
<bit-select [items]="typeOptions$ | async" formControlName="nav"> </bit-select>
<bit-select
[items]="typeOptions$ | async"
formControlName="nav"
data-testid="username-type"
>
</bit-select>
<bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{
credentialTypeHint$ | async
}}</bit-hint>
@@ -42,7 +47,12 @@
<form *ngIf="showForwarder$ | async" [formGroup]="forwarder" class="box tw-container">
<bit-form-field>
<bit-label>{{ "service" | i18n }}</bit-label>
<bit-select [items]="forwarderOptions$ | async" formControlName="nav"> </bit-select>
<bit-select
[items]="forwarderOptions$ | async"
formControlName="nav"
data-testid="email-forwarding-service"
>
</bit-select>
</bit-form-field>
</form>
<tools-catchall-settings

View File

@@ -36,6 +36,7 @@ import {
isUsernameAlgorithm,
toCredentialGeneratorConfiguration,
} from "@bitwarden/generator-core";
import { GeneratorHistoryService } from "@bitwarden/generator-history";
// constants used to identify navigation selections that are not
// generator algorithms
@@ -57,6 +58,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
*/
constructor(
private generatorService: CredentialGeneratorService,
private generatorHistoryService: GeneratorHistoryService,
private toastService: ToastService,
private logService: LogService,
private i18nService: I18nService,
@@ -153,9 +155,16 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
// continue with origin stream
return generator;
}),
withLatestFrom(this.userId$),
takeUntil(this.destroyed),
)
.subscribe((generated) => {
.subscribe(([generated, userId]) => {
this.generatorHistoryService
.track(userId, generated.credential, generated.category, generated.generationDate)
.catch((e: unknown) => {
this.logService.error(e);
});
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {

View File

@@ -1,6 +1,6 @@
import { Jsonify } from "type-fest";
import { GeneratorCategory } from "./options";
import { CredentialAlgorithm } from "@bitwarden/generator-core";
/** A credential generation result */
export class GeneratedCredential {
@@ -14,7 +14,7 @@ export class GeneratedCredential {
*/
constructor(
readonly credential: string,
readonly category: GeneratorCategory,
readonly category: CredentialAlgorithm,
generationDate: Date | number,
) {
if (typeof generationDate === "number") {

View File

@@ -1,9 +1,9 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
import { CredentialAlgorithm } from "@bitwarden/generator-core";
import { GeneratedCredential } from "./generated-credential";
import { GeneratorCategory } from "./options";
/** Tracks the history of password generations.
* Each user gets their own store.
@@ -27,7 +27,7 @@ export abstract class GeneratorHistoryService {
track: (
userId: UserId,
credential: string,
category: GeneratorCategory,
category: CredentialAlgorithm,
date?: Date,
) => Promise<GeneratedCredential | null>;

View File

@@ -8,12 +8,13 @@ import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-pack
import { SecretState } from "@bitwarden/common/tools/state/secret-state";
import { UserKeyEncryptor } from "@bitwarden/common/tools/state/user-key-encryptor";
import { UserId } from "@bitwarden/common/types/guid";
import { CredentialAlgorithm } from "@bitwarden/generator-core";
import { GeneratedCredential } from "./generated-credential";
import { GeneratorHistoryService } from "./generator-history.abstraction";
import { GENERATOR_HISTORY, GENERATOR_HISTORY_BUFFER } from "./key-definitions";
import { LegacyPasswordHistoryDecryptor } from "./legacy-password-history-decryptor";
import { GeneratorCategory, HistoryServiceOptions } from "./options";
import { HistoryServiceOptions } from "./options";
const OPTIONS_FRAME_SIZE = 2048;
@@ -25,7 +26,7 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
private readonly encryptService: EncryptService,
private readonly keyService: CryptoService,
private readonly stateProvider: StateProvider,
private readonly options: HistoryServiceOptions = { maxTotal: 100 },
private readonly options: HistoryServiceOptions = { maxTotal: 200 },
) {
super();
}
@@ -33,7 +34,12 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService {
private _credentialStates = new Map<UserId, SingleUserState<GeneratedCredential[]>>();
/** {@link GeneratorHistoryService.track} */
track = async (userId: UserId, credential: string, category: GeneratorCategory, date?: Date) => {
track = async (
userId: UserId,
credential: string,
category: CredentialAlgorithm,
date?: Date,
) => {
const state = this.getCredentialState(userId);
let result: GeneratedCredential = null;

View File

@@ -3,6 +3,7 @@ import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import {
CardComponent,
@@ -30,10 +31,15 @@ import {
})
export class AutofillOptionsViewComponent {
@Input() loginUris: LoginUriView[];
@Input() cipherId: string;
constructor(private platformUtilsService: PlatformUtilsService) {}
constructor(
private platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
) {}
openWebsite(selectedUri: string) {
async openWebsite(selectedUri: string) {
await this.cipherService.updateLastLaunchedDate(this.cipherId);
this.platformUtilsService.launchUri(selectedUri);
}
}

View File

@@ -25,7 +25,11 @@
<app-login-credentials-view *ngIf="hasLogin" [cipher]="cipher"></app-login-credentials-view>
<!-- AUTOFILL OPTIONS -->
<app-autofill-options-view *ngIf="hasAutofill" [loginUris]="cipher.login.uris">
<app-autofill-options-view
*ngIf="hasAutofill"
[loginUris]="cipher.login.uris"
[cipherId]="cipher.id"
>
</app-autofill-options-view>
<!-- CARD DETAILS -->