1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-30 16:23:53 +00:00

Merge branch 'main' into auth/pm-26578/http-redirect-cloud

This commit is contained in:
Patrick-Pimentel-Bitwarden
2026-01-08 19:37:47 -05:00
committed by GitHub
13 changed files with 197 additions and 33 deletions

View File

@@ -1086,15 +1086,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
pageDetails,
)
) {
const hasUsernameField = [...this.formFieldElements.values()].some((field) =>
this.inlineMenuFieldQualificationService.isUsernameField(field),
);
if (hasUsernameField) {
void this.setQualifiedLoginFillType(autofillFieldData);
} else {
this.setQualifiedAccountCreationFillType(autofillFieldData);
}
this.setQualifiedAccountCreationFillType(autofillFieldData);
return false;
}

View File

@@ -18,8 +18,7 @@ export const PHISHING_RESOURCES: Record<PhishingResourceType, PhishingResource[]
[PhishingResourceType.Domains]: [
{
name: "Phishing.Database Domains",
remoteUrl:
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/master/phishing-domains-ACTIVE.txt",
remoteUrl: "https://phish.co.za/latest/phishing-domains-ACTIVE.txt",
checksumUrl:
"https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.md5",
todayUrl:
@@ -46,8 +45,7 @@ export const PHISHING_RESOURCES: Record<PhishingResourceType, PhishingResource[]
[PhishingResourceType.Links]: [
{
name: "Phishing.Database Links",
remoteUrl:
"https://raw.githubusercontent.com/Phishing-Database/Phishing.Database/master/phishing-links-ACTIVE.txt",
remoteUrl: "https://phish.co.za/latest/phishing-links-ACTIVE.txt",
checksumUrl:
"https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-links-ACTIVE.txt.md5",
todayUrl:

View File

@@ -86,6 +86,7 @@ import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/v
import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import {
atRiskPasswordAuthGuard,
canAccessAtRiskPasswords,
hasAtRiskPasswords,
} from "../vault/popup/guards/at-risk-passwords.guard";
@@ -723,7 +724,7 @@ const routes: Routes = [
{
path: "at-risk-passwords",
component: AtRiskPasswordsComponent,
canActivate: [authGuard, canAccessAtRiskPasswords, hasAtRiskPasswords],
canActivate: [atRiskPasswordAuthGuard, canAccessAtRiskPasswords, hasAtRiskPasswords],
},
{
path: AuthExtensionRoute.AccountSwitcher,

View File

@@ -1,7 +1,13 @@
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from "@angular/router";
import { combineLatest, map, switchMap } from "rxjs";
import { authGuard } from "@bitwarden/angular/auth/guards";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -9,6 +15,24 @@ import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
import { ToastService } from "@bitwarden/components";
/**
* Wrapper around the main auth guard to redirect to login if not authenticated.
* This is necessary because the main auth guard returns false when not authenticated,
* which in a browser context may result in a blank extension page rather than a redirect.
*/
export const atRiskPasswordAuthGuard: CanActivateFn = async (
route: ActivatedRouteSnapshot,
routerState: RouterStateSnapshot,
) => {
const router = inject(Router);
const authGuardResponse = await authGuard(route, routerState);
if (authGuardResponse === true) {
return authGuardResponse;
}
return router.createUrlTree(["/login"]);
};
export const canAccessAtRiskPasswords: CanActivateFn = () => {
const accountService = inject(AccountService);
const taskService = inject(TaskService);

View File

@@ -34,7 +34,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { getByIds } from "@bitwarden/common/platform/misc";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -158,7 +158,7 @@ export class VaultComponent<C extends CipherViewLike>
cipherId: string | null = null;
favorites = false;
type: CipherType | null = null;
folderId: string | null = null;
folderId: string | null | undefined = null;
collectionId: string | null = null;
organizationId: string | null = null;
myVaultOnly = false;
@@ -980,9 +980,7 @@ export class VaultComponent<C extends CipherViewLike>
// clear out organizationId when the user switches to a personal vault filter
this.addOrganizationId = null;
}
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
this.folderId = this.activeFilter.selectedFolderId;
}
this.folderId = this.activeFilter.selectedFolderId;
if (this.config == null) {
return;
@@ -990,7 +988,9 @@ export class VaultComponent<C extends CipherViewLike>
this.config.initialValues = {
...this.config.initialValues,
folderId: this.folderId,
organizationId: this.addOrganizationId as OrganizationId,
collectionIds: this.addCollectionIds as CollectionId[],
};
}

View File

@@ -45,7 +45,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { getByIds } from "@bitwarden/common/platform/misc";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -172,7 +172,7 @@ export class VaultV2Component<C extends CipherViewLike>
cipherId: string | null = null;
favorites = false;
type: CipherType | null = null;
folderId: string | null = null;
folderId: string | null | undefined = null;
collectionId: string | null = null;
organizationId: OrganizationId | null = null;
myVaultOnly = false;
@@ -1016,9 +1016,7 @@ export class VaultV2Component<C extends CipherViewLike>
// clear out organizationId when the user switches to a personal vault filter
this.addOrganizationId = null;
}
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
this.folderId = this.activeFilter.selectedFolderId;
}
this.folderId = this.activeFilter.selectedFolderId;
if (this.config == null) {
return;
@@ -1027,6 +1025,8 @@ export class VaultV2Component<C extends CipherViewLike>
this.config.initialValues = {
...this.config.initialValues,
organizationId: this.addOrganizationId as OrganizationId,
folderId: this.folderId,
collectionIds: this.addCollectionIds as CollectionId[],
};
}

View File

@@ -43,7 +43,9 @@
<div>
{{ "premiumSubscriptionEndedDesc" | i18n }}
</div>
<a bitLink (click)="navigateToGetPremium()"> {{ "restartPremium" | i18n }} </a>
<a bitLink href="#" appStopClick (click)="navigateToGetPremium()">
{{ "restartPremium" | i18n }}
</a>
</ng-container>
</bit-callout>
}

View File

@@ -20,6 +20,16 @@ export abstract class CipherEncryptionService {
*/
abstract encrypt(model: CipherView, userId: UserId): Promise<EncryptionContext | undefined>;
/**
* Encrypts multiple ciphers using the SDK for the given userId.
*
* @param models The cipher views to encrypt
* @param userId The user ID to initialize the SDK client with
*
* @returns A promise that resolves to an array of encryption contexts
*/
abstract encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]>;
/**
* Move the cipher to the specified organization by re-encrypting its keys with the organization's key.
* The cipher.organizationId will be updated to the new organizationId.

View File

@@ -50,6 +50,15 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
keyForCipherKeyDecryption?: SymmetricCryptoKey,
originalCipher?: Cipher,
): Promise<EncryptionContext>;
/**
* Encrypts multiple ciphers for the given user.
*
* @param models The cipher views to encrypt
* @param userId The user ID to encrypt for
*
* @returns A promise that resolves to an array of encryption contexts
*/
abstract encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]>;
abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise<Field[]>;
abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise<Field>;
abstract get(id: string, userId: UserId): Promise<Cipher>;

View File

@@ -340,6 +340,24 @@ export class CipherService implements CipherServiceAbstraction {
}
}
async encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]> {
const sdkEncryptionEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM22136_SdkCipherEncryption,
);
if (sdkEncryptionEnabled) {
return await this.cipherEncryptionService.encryptMany(models, userId);
}
// Fallback to sequential encryption if SDK disabled
const results: EncryptionContext[] = [];
for (const model of models) {
const result = await this.encrypt(model, userId);
results.push(result);
}
return results;
}
async encryptAttachments(
attachmentsModel: AttachmentView[],
key: SymmetricCryptoKey,

View File

@@ -253,6 +253,68 @@ describe("DefaultCipherEncryptionService", () => {
});
});
describe("encryptMany", () => {
it("should encrypt multiple ciphers", async () => {
const cipherView2 = new CipherView(cipherObj);
cipherView2.name = "test-name-2";
const cipherView3 = new CipherView(cipherObj);
cipherView3.name = "test-name-3";
const ciphers = [cipherViewObj, cipherView2, cipherView3];
const expectedCipher1: Cipher = {
id: cipherId as string,
type: CipherType.Login,
name: "encrypted-name-1",
} as unknown as Cipher;
const expectedCipher2: Cipher = {
id: cipherId as string,
type: CipherType.Login,
name: "encrypted-name-2",
} as unknown as Cipher;
const expectedCipher3: Cipher = {
id: cipherId as string,
type: CipherType.Login,
name: "encrypted-name-3",
} as unknown as Cipher;
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
cipher: sdkCipher,
encryptedFor: userId,
});
jest
.spyOn(Cipher, "fromSdkCipher")
.mockReturnValueOnce(expectedCipher1)
.mockReturnValueOnce(expectedCipher2)
.mockReturnValueOnce(expectedCipher3);
const results = await cipherEncryptionService.encryptMany(ciphers, userId);
expect(results).toBeDefined();
expect(results.length).toBe(3);
expect(results[0].cipher).toEqual(expectedCipher1);
expect(results[1].cipher).toEqual(expectedCipher2);
expect(results[2].cipher).toEqual(expectedCipher3);
expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledTimes(3);
expect(results[0].encryptedFor).toBe(userId);
expect(results[1].encryptedFor).toBe(userId);
expect(results[2].encryptedFor).toBe(userId);
});
it("should handle empty array", async () => {
const results = await cipherEncryptionService.encryptMany([], userId);
expect(results).toBeDefined();
expect(results.length).toBe(0);
expect(mockSdkClient.vault().ciphers().encrypt).not.toHaveBeenCalled();
});
});
describe("encryptCipherForRotation", () => {
it("should call the sdk method to encrypt the cipher with a new key for rotation", async () => {
mockSdkClient.vault().ciphers().encrypt_cipher_for_rotation.mockReturnValue({

View File

@@ -51,6 +51,44 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
);
}
async encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]> {
if (!models || models.length === 0) {
return [];
}
return firstValueFrom(
this.sdkService.userClient$(userId).pipe(
map((sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
const results: EncryptionContext[] = [];
// TODO: https://bitwarden.atlassian.net/browse/PM-30580
// Replace this loop with a native SDK encryptMany method for better performance.
for (const model of models) {
const sdkCipherView = this.toSdkCipherView(model, ref.value);
const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView);
results.push({
cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId,
});
}
return results;
}),
catchError((error: unknown) => {
this.logService.error(`Failed to encrypt ciphers in batch: ${error}`);
return EMPTY;
}),
),
);
}
async moveToOrganization(
model: CipherView,
organizationId: OrganizationId,

View File

@@ -374,10 +374,13 @@ export class ImportService implements ImportServiceAbstraction {
private async handleIndividualImport(importResult: ImportResult, userId: UserId) {
const request = new ImportCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
const c = await this.cipherService.encrypt(importResult.ciphers[i], userId);
request.ciphers.push(new CipherRequest(c));
const encryptedCiphers = await this.cipherService.encryptMany(importResult.ciphers, userId);
for (const encryptedCipher of encryptedCiphers) {
request.ciphers.push(new CipherRequest(encryptedCipher));
}
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
if (importResult.folders != null) {
@@ -400,11 +403,18 @@ export class ImportService implements ImportServiceAbstraction {
userId: UserId,
) {
const request = new ImportOrganizationCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
importResult.ciphers[i].organizationId = organizationId;
const c = await this.cipherService.encrypt(importResult.ciphers[i], userId);
request.ciphers.push(new CipherRequest(c));
// Set organization ID on all ciphers before batch encryption
importResult.ciphers.forEach((cipher) => {
cipher.organizationId = organizationId;
});
const encryptedCiphers = await this.cipherService.encryptMany(importResult.ciphers, userId);
for (const encryptedCipher of encryptedCiphers) {
request.ciphers.push(new CipherRequest(encryptedCipher));
}
if (importResult.collections != null) {
for (let i = 0; i < importResult.collections.length; i++) {
importResult.collections[i].organizationId = organizationId;