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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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[],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user