mirror of
https://github.com/bitwarden/browser
synced 2026-02-27 01:53:23 +00:00
Merge remote-tracking branch 'origin' into auth/pm-19877/notification-processing
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
|
||||
@@ -32,6 +33,18 @@ export abstract class CipherEncryptionService {
|
||||
userId: UserId,
|
||||
): Promise<EncryptionContext | undefined>;
|
||||
|
||||
/**
|
||||
* Encrypts a cipher for a given userId with a new key for key rotation.
|
||||
* @param model The cipher view to encrypt
|
||||
* @param userId The user ID to initialize the SDK client with
|
||||
* @param newKey The new key to use for re-encryption
|
||||
*/
|
||||
abstract encryptCipherForRotation(
|
||||
model: CipherView,
|
||||
userId: UserId,
|
||||
newKey: UserKey,
|
||||
): Promise<EncryptionContext | undefined>;
|
||||
|
||||
/**
|
||||
* Decrypts a cipher using the SDK for the given userId.
|
||||
*
|
||||
|
||||
@@ -110,7 +110,7 @@ describe("Attachment", () => {
|
||||
|
||||
await attachment.decrypt(null, "", providedKey);
|
||||
|
||||
expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
|
||||
expect(keyService.getUserKey).not.toHaveBeenCalled();
|
||||
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(attachment.key, providedKey);
|
||||
});
|
||||
|
||||
@@ -126,11 +126,11 @@ describe("Attachment", () => {
|
||||
|
||||
it("gets the user's decryption key if required", async () => {
|
||||
const userKey = mock<UserKey>();
|
||||
keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
|
||||
keyService.getUserKey.mockResolvedValue(userKey);
|
||||
|
||||
await attachment.decrypt(null, "", null);
|
||||
|
||||
expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalled();
|
||||
expect(keyService.getUserKey).toHaveBeenCalled();
|
||||
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(attachment.key, userKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,9 +80,7 @@ export class Attachment extends Domain {
|
||||
|
||||
private async getKeyForDecryption(orgId: string) {
|
||||
const keyService = Utils.getContainerService().getKeyService();
|
||||
return orgId != null
|
||||
? await keyService.getOrgKey(orgId)
|
||||
: await keyService.getUserKeyWithLegacySupport();
|
||||
return orgId != null ? await keyService.getOrgKey(orgId) : await keyService.getUserKey();
|
||||
}
|
||||
|
||||
toAttachmentData(): AttachmentData {
|
||||
@@ -128,8 +126,8 @@ export class Attachment extends Domain {
|
||||
url: this.url,
|
||||
size: this.size,
|
||||
sizeName: this.sizeName,
|
||||
fileName: this.fileName?.toJSON(),
|
||||
key: this.key?.toJSON(),
|
||||
fileName: this.fileName?.toSdk(),
|
||||
key: this.key?.toSdk(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -95,12 +95,12 @@ export class Card extends Domain {
|
||||
*/
|
||||
toSdkCard(): SdkCard {
|
||||
return {
|
||||
cardholderName: this.cardholderName?.toJSON(),
|
||||
brand: this.brand?.toJSON(),
|
||||
number: this.number?.toJSON(),
|
||||
expMonth: this.expMonth?.toJSON(),
|
||||
expYear: this.expYear?.toJSON(),
|
||||
code: this.code?.toJSON(),
|
||||
cardholderName: this.cardholderName?.toSdk(),
|
||||
brand: this.brand?.toSdk(),
|
||||
number: this.number?.toSdk(),
|
||||
expMonth: this.expMonth?.toSdk(),
|
||||
expYear: this.expYear?.toSdk(),
|
||||
code: this.code?.toSdk(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CipherRepromptType as SdkCipherRepromptType,
|
||||
LoginLinkedIdType,
|
||||
Cipher as SdkCipher,
|
||||
EncString as SdkEncString,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
|
||||
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
@@ -1010,22 +1011,22 @@ describe("Cipher DTO", () => {
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
collectionIds: [],
|
||||
key: "EncryptedString",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
key: "EncryptedString" as SdkEncString,
|
||||
name: "EncryptedString" as SdkEncString,
|
||||
notes: "EncryptedString" as SdkEncString,
|
||||
type: SdkCipherType.Login,
|
||||
login: {
|
||||
username: "EncryptedString",
|
||||
password: "EncryptedString",
|
||||
username: "EncryptedString" as SdkEncString,
|
||||
password: "EncryptedString" as SdkEncString,
|
||||
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||
uris: [
|
||||
{
|
||||
uri: "EncryptedString",
|
||||
uriChecksum: "EncryptedString",
|
||||
uri: "EncryptedString" as SdkEncString,
|
||||
uriChecksum: "EncryptedString" as SdkEncString,
|
||||
match: UriMatchType.Domain,
|
||||
},
|
||||
],
|
||||
totp: "EncryptedString",
|
||||
totp: "EncryptedString" as SdkEncString,
|
||||
autofillOnPageLoad: false,
|
||||
fido2Credentials: undefined,
|
||||
},
|
||||
@@ -1049,35 +1050,35 @@ describe("Cipher DTO", () => {
|
||||
url: "url",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "file",
|
||||
key: "EncKey",
|
||||
fileName: "file" as SdkEncString,
|
||||
key: "EncKey" as SdkEncString,
|
||||
},
|
||||
{
|
||||
id: "a2",
|
||||
url: "url",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "file",
|
||||
key: "EncKey",
|
||||
fileName: "file" as SdkEncString,
|
||||
key: "EncKey" as SdkEncString,
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: "EncryptedString",
|
||||
value: "EncryptedString",
|
||||
name: "EncryptedString" as SdkEncString,
|
||||
value: "EncryptedString" as SdkEncString,
|
||||
type: FieldType.Linked,
|
||||
linkedId: LoginLinkedIdType.Username,
|
||||
},
|
||||
{
|
||||
name: "EncryptedString",
|
||||
value: "EncryptedString",
|
||||
name: "EncryptedString" as SdkEncString,
|
||||
value: "EncryptedString" as SdkEncString,
|
||||
type: FieldType.Linked,
|
||||
linkedId: LoginLinkedIdType.Password,
|
||||
},
|
||||
],
|
||||
passwordHistory: [
|
||||
{
|
||||
password: "EncryptedString",
|
||||
password: "EncryptedString" as SdkEncString,
|
||||
lastUsedDate: "2022-01-31T12:00:00.000Z",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -348,9 +348,9 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
organizationId: this.organizationId ?? undefined,
|
||||
folderId: this.folderId ?? undefined,
|
||||
collectionIds: this.collectionIds ?? [],
|
||||
key: this.key?.toJSON(),
|
||||
name: this.name.toJSON(),
|
||||
notes: this.notes?.toJSON(),
|
||||
key: this.key?.toSdk(),
|
||||
name: this.name.toSdk(),
|
||||
notes: this.notes?.toSdk(),
|
||||
type: this.type,
|
||||
favorite: this.favorite ?? false,
|
||||
organizationUseTotp: this.organizationUseTotp ?? false,
|
||||
|
||||
@@ -158,18 +158,18 @@ export class Fido2Credential extends Domain {
|
||||
*/
|
||||
toSdkFido2Credential(): SdkFido2Credential {
|
||||
return {
|
||||
credentialId: this.credentialId?.toJSON(),
|
||||
keyType: this.keyType.toJSON(),
|
||||
keyAlgorithm: this.keyAlgorithm.toJSON(),
|
||||
keyCurve: this.keyCurve.toJSON(),
|
||||
keyValue: this.keyValue.toJSON(),
|
||||
rpId: this.rpId.toJSON(),
|
||||
userHandle: this.userHandle?.toJSON(),
|
||||
userName: this.userName?.toJSON(),
|
||||
counter: this.counter.toJSON(),
|
||||
rpName: this.rpName?.toJSON(),
|
||||
userDisplayName: this.userDisplayName?.toJSON(),
|
||||
discoverable: this.discoverable?.toJSON(),
|
||||
credentialId: this.credentialId?.toSdk(),
|
||||
keyType: this.keyType.toSdk(),
|
||||
keyAlgorithm: this.keyAlgorithm.toSdk(),
|
||||
keyCurve: this.keyCurve.toSdk(),
|
||||
keyValue: this.keyValue.toSdk(),
|
||||
rpId: this.rpId.toSdk(),
|
||||
userHandle: this.userHandle?.toSdk(),
|
||||
userName: this.userName?.toSdk(),
|
||||
counter: this.counter.toSdk(),
|
||||
rpName: this.rpName?.toSdk(),
|
||||
userDisplayName: this.userDisplayName?.toSdk(),
|
||||
discoverable: this.discoverable?.toSdk(),
|
||||
creationDate: this.creationDate.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ export class Field extends Domain {
|
||||
*/
|
||||
toSdkField(): SdkField {
|
||||
return {
|
||||
name: this.name?.toJSON(),
|
||||
value: this.value?.toJSON(),
|
||||
name: this.name?.toSdk(),
|
||||
value: this.value?.toSdk(),
|
||||
type: this.type,
|
||||
// Safe type cast: client and SDK LinkedIdType enums have identical values
|
||||
linkedId: this.linkedId as unknown as SdkLinkedIdType,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { makeEncString, makeSymmetricCryptoKey, mockEnc, mockFromJson } from "../../../../spec";
|
||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
|
||||
@@ -73,13 +71,8 @@ describe("Folder", () => {
|
||||
beforeEach(() => {
|
||||
encryptService = mock<EncryptService>();
|
||||
// Platform code is not migrated yet
|
||||
encryptService.decryptToUtf8.mockImplementation(
|
||||
(value: EncString, key: SymmetricCryptoKey, decryptTrace: string) => {
|
||||
return Promise.resolve(value.data);
|
||||
},
|
||||
);
|
||||
encryptService.decryptString.mockImplementation((value) => {
|
||||
return Promise.resolve(value.data);
|
||||
encryptService.decryptString.mockImplementation((_value, _key) => {
|
||||
return Promise.resolve("encName");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ export class Folder extends Domain {
|
||||
key: SymmetricCryptoKey,
|
||||
encryptService: EncryptService,
|
||||
): Promise<FolderView> {
|
||||
const decrypted = await this.decryptObjWithKey(["name"], key, encryptService, Folder);
|
||||
|
||||
const view = new FolderView(decrypted);
|
||||
view.name = decrypted.name;
|
||||
return view;
|
||||
const folderView = new FolderView();
|
||||
folderView.id = this.id;
|
||||
folderView.revisionDate = this.revisionDate;
|
||||
folderView.name = await encryptService.decryptString(this.name, key);
|
||||
return folderView;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<Folder>) {
|
||||
|
||||
@@ -175,24 +175,24 @@ export class Identity extends Domain {
|
||||
*/
|
||||
toSdkIdentity(): SdkIdentity {
|
||||
return {
|
||||
title: this.title?.toJSON(),
|
||||
firstName: this.firstName?.toJSON(),
|
||||
middleName: this.middleName?.toJSON(),
|
||||
lastName: this.lastName?.toJSON(),
|
||||
address1: this.address1?.toJSON(),
|
||||
address2: this.address2?.toJSON(),
|
||||
address3: this.address3?.toJSON(),
|
||||
city: this.city?.toJSON(),
|
||||
state: this.state?.toJSON(),
|
||||
postalCode: this.postalCode?.toJSON(),
|
||||
country: this.country?.toJSON(),
|
||||
company: this.company?.toJSON(),
|
||||
email: this.email?.toJSON(),
|
||||
phone: this.phone?.toJSON(),
|
||||
ssn: this.ssn?.toJSON(),
|
||||
username: this.username?.toJSON(),
|
||||
passportNumber: this.passportNumber?.toJSON(),
|
||||
licenseNumber: this.licenseNumber?.toJSON(),
|
||||
title: this.title?.toSdk(),
|
||||
firstName: this.firstName?.toSdk(),
|
||||
middleName: this.middleName?.toSdk(),
|
||||
lastName: this.lastName?.toSdk(),
|
||||
address1: this.address1?.toSdk(),
|
||||
address2: this.address2?.toSdk(),
|
||||
address3: this.address3?.toSdk(),
|
||||
city: this.city?.toSdk(),
|
||||
state: this.state?.toSdk(),
|
||||
postalCode: this.postalCode?.toSdk(),
|
||||
country: this.country?.toSdk(),
|
||||
company: this.company?.toSdk(),
|
||||
email: this.email?.toSdk(),
|
||||
phone: this.phone?.toSdk(),
|
||||
ssn: this.ssn?.toSdk(),
|
||||
username: this.username?.toSdk(),
|
||||
passportNumber: this.passportNumber?.toSdk(),
|
||||
licenseNumber: this.licenseNumber?.toSdk(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -97,8 +97,8 @@ export class LoginUri extends Domain {
|
||||
*/
|
||||
toSdkLoginUri(): SdkLoginUri {
|
||||
return {
|
||||
uri: this.uri?.toJSON(),
|
||||
uriChecksum: this.uriChecksum?.toJSON(),
|
||||
uri: this.uri?.toSdk(),
|
||||
uriChecksum: this.uriChecksum?.toSdk(),
|
||||
match: this.match,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,10 +155,10 @@ export class Login extends Domain {
|
||||
toSdkLogin(): SdkLogin {
|
||||
return {
|
||||
uris: this.uris?.map((u) => u.toSdkLoginUri()),
|
||||
username: this.username?.toJSON(),
|
||||
password: this.password?.toJSON(),
|
||||
username: this.username?.toSdk(),
|
||||
password: this.password?.toSdk(),
|
||||
passwordRevisionDate: this.passwordRevisionDate?.toISOString(),
|
||||
totp: this.totp?.toJSON(),
|
||||
totp: this.totp?.toSdk(),
|
||||
autofillOnPageLoad: this.autofillOnPageLoad ?? undefined,
|
||||
fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()),
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ export class Password extends Domain {
|
||||
*/
|
||||
toSdkPasswordHistory(): PasswordHistory {
|
||||
return {
|
||||
password: this.password.toJSON(),
|
||||
password: this.password.toSdk(),
|
||||
lastUsedDate: this.lastUsedDate.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,9 +80,9 @@ export class SshKey extends Domain {
|
||||
*/
|
||||
toSdkSshKey(): SdkSshKey {
|
||||
return {
|
||||
privateKey: this.privateKey.toJSON(),
|
||||
publicKey: this.publicKey.toJSON(),
|
||||
fingerprint: this.keyFingerprint.toJSON(),
|
||||
privateKey: this.privateKey.toSdk(),
|
||||
publicKey: this.publicKey.toSdk(),
|
||||
fingerprint: this.keyFingerprint.toSdk(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export class AttachmentView implements View {
|
||||
size: this.size,
|
||||
sizeName: this.sizeName,
|
||||
fileName: this.fileName,
|
||||
key: this.encryptedKey?.toJSON(),
|
||||
key: this.encryptedKey?.toSdk(),
|
||||
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
|
||||
decryptedKey: this.key ? this.key.toBase64() : null,
|
||||
};
|
||||
|
||||
@@ -331,7 +331,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
creationDate: (this.creationDate ?? new Date()).toISOString(),
|
||||
deletedDate: this.deletedDate?.toISOString(),
|
||||
reprompt: this.reprompt ?? CipherRepromptType.None,
|
||||
key: this.key?.toJSON(),
|
||||
key: this.key?.toSdk(),
|
||||
// Cipher type specific properties are set in the switch statement below
|
||||
// CipherView initializes each with default constructors (undefined values)
|
||||
// The SDK does not expect those undefined values and will throw exceptions
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("CipherAuthorizationService", () => {
|
||||
|
||||
cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => {
|
||||
expect(result).toBe(false);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||
expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -133,7 +133,7 @@ describe("CipherAuthorizationService", () => {
|
||||
|
||||
cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => {
|
||||
expect(result).toBe(true);
|
||||
expect(mockCollectionService.decryptedCollectionViews$).not.toHaveBeenCalled();
|
||||
expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -198,6 +198,7 @@ describe("CipherAuthorizationService", () => {
|
||||
|
||||
cipherAuthorizationService.canDeleteCipher$(cipher, false).subscribe((result) => {
|
||||
expect(result).toBe(false);
|
||||
expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -251,7 +252,7 @@ describe("CipherAuthorizationService", () => {
|
||||
createMockCollection("col1", true),
|
||||
createMockCollection("col2", false),
|
||||
];
|
||||
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
|
||||
mockCollectionService.decryptedCollections$.mockReturnValue(
|
||||
of(allCollections as CollectionView[]),
|
||||
);
|
||||
|
||||
@@ -270,7 +271,7 @@ describe("CipherAuthorizationService", () => {
|
||||
createMockCollection("col1", false),
|
||||
createMockCollection("col2", false),
|
||||
];
|
||||
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
|
||||
mockCollectionService.decryptedCollections$.mockReturnValue(
|
||||
of(allCollections as CollectionView[]),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||
import { combineLatest, map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { getByIds } from "@bitwarden/common/platform/misc";
|
||||
|
||||
import { getUserId } from "../../auth/services/account.service";
|
||||
import { CipherLike } from "../types/cipher-like";
|
||||
@@ -125,8 +125,11 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return this.organization$(cipher).pipe(
|
||||
switchMap((organization) => {
|
||||
return combineLatest([
|
||||
this.organization$(cipher),
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
]).pipe(
|
||||
switchMap(([organization, userId]) => {
|
||||
// Admins and custom users can always clone when in the Admin Console
|
||||
if (
|
||||
isAdminConsoleAction &&
|
||||
@@ -136,9 +139,10 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
|
||||
return of(true);
|
||||
}
|
||||
|
||||
return this.collectionService
|
||||
.decryptedCollectionViews$(cipher.collectionIds as CollectionId[])
|
||||
.pipe(map((allCollections) => allCollections.some((collection) => collection.manage)));
|
||||
return this.collectionService.decryptedCollections$(userId).pipe(
|
||||
getByIds(cipher.collectionIds),
|
||||
map((allCollections) => allCollections.some((collection) => collection.manage)),
|
||||
);
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
);
|
||||
|
||||
@@ -14,7 +14,6 @@ import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
|
||||
import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
||||
@@ -102,7 +101,6 @@ describe("Cipher Service", () => {
|
||||
const i18nService = mock<I18nService>();
|
||||
const searchService = mock<SearchService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const bulkEncryptService = mock<BulkEncryptService>();
|
||||
const configService = mock<ConfigService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
const logService = mock<LogService>();
|
||||
@@ -130,7 +128,6 @@ describe("Cipher Service", () => {
|
||||
stateService,
|
||||
autofillSettingsService,
|
||||
encryptService,
|
||||
bulkEncryptService,
|
||||
cipherFileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
@@ -397,7 +394,7 @@ describe("Cipher Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptWithCipherKey", () => {
|
||||
describe("encryptCipherForRotation", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn<any, string>(cipherService, "encryptCipherWithCipherKey");
|
||||
keyService.getOrgKey.mockReturnValue(
|
||||
@@ -534,6 +531,26 @@ describe("Cipher Service", () => {
|
||||
cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId),
|
||||
).rejects.toThrow("Cannot rotate ciphers when decryption failures are present");
|
||||
});
|
||||
|
||||
it("uses the sdk to re-encrypt ciphers when feature flag is enabled", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM22136_SdkCipherEncryption)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
cipherEncryptionService.encryptCipherForRotation.mockResolvedValue({
|
||||
cipher: encryptionContext.cipher,
|
||||
encryptedFor: mockUserId,
|
||||
});
|
||||
|
||||
const result = await cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(cipherEncryptionService.encryptCipherForRotation).toHaveBeenCalledWith(
|
||||
expect.any(CipherView),
|
||||
mockUserId,
|
||||
newUserKey,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decrypt", () => {
|
||||
@@ -558,7 +575,6 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM19941MigrateCipherDomainToSdk)
|
||||
.mockResolvedValue(false);
|
||||
cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey);
|
||||
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
|
||||
jest
|
||||
.spyOn(encryptionContext.cipher, "decrypt")
|
||||
.mockResolvedValue(new CipherView(encryptionContext.cipher));
|
||||
|
||||
@@ -13,7 +13,6 @@ import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
|
||||
import { FeatureFlag } from "../../enums/feature-flag.enum";
|
||||
import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
|
||||
@@ -104,7 +103,6 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private stateService: StateService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private encryptService: EncryptService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
private cipherFileUploadService: CipherFileUploadService,
|
||||
private configService: ConfigService,
|
||||
private stateProvider: StateProvider,
|
||||
@@ -172,7 +170,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return combineLatest([
|
||||
this.encryptedCiphersState(userId).state$,
|
||||
this.localData$(userId),
|
||||
this.keyService.cipherDecryptionKeys$(userId, true),
|
||||
this.keyService.cipherDecryptionKeys$(userId),
|
||||
]).pipe(
|
||||
filter(([ciphers, _, keys]) => ciphers != null && keys != null), // Skip if ciphers haven't been loaded yor synced yet
|
||||
switchMap(() => this.getAllDecrypted(userId)),
|
||||
@@ -488,7 +486,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return [decrypted, []];
|
||||
}
|
||||
|
||||
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
|
||||
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId));
|
||||
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
|
||||
// return early if there are no keys to decrypt with
|
||||
return null;
|
||||
@@ -506,17 +504,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
const allCipherViews = (
|
||||
await Promise.all(
|
||||
Object.entries(grouped).map(async ([orgId, groupedCiphers]) => {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
|
||||
return await this.bulkEncryptService.decryptItems(
|
||||
groupedCiphers,
|
||||
keys.orgKeys?.[orgId as OrganizationId] ?? keys.userKey,
|
||||
);
|
||||
} else {
|
||||
return await this.encryptService.decryptItems(
|
||||
groupedCiphers,
|
||||
keys.orgKeys?.[orgId as OrganizationId] ?? keys.userKey,
|
||||
);
|
||||
}
|
||||
const key = keys.orgKeys[orgId as OrganizationId] ?? keys.userKey;
|
||||
return await Promise.all(
|
||||
groupedCiphers.map(async (cipher) => {
|
||||
return await cipher.decrypt(key);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
)
|
||||
)
|
||||
@@ -684,12 +677,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr)));
|
||||
const key = await this.keyService.getOrgKey(organizationId);
|
||||
let decCiphers: CipherView[] = [];
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
|
||||
decCiphers = await this.bulkEncryptService.decryptItems(ciphers, key);
|
||||
} else {
|
||||
decCiphers = await this.encryptService.decryptItems(ciphers, key);
|
||||
}
|
||||
const decCiphers: CipherView[] = await Promise.all(
|
||||
ciphers.map(async (cipher) => {
|
||||
return await cipher.decrypt(key);
|
||||
}),
|
||||
);
|
||||
|
||||
decCiphers.sort(this.getLocaleSortingFunction());
|
||||
return decCiphers;
|
||||
@@ -1474,7 +1466,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> {
|
||||
return (
|
||||
(await this.keyService.getOrgKey(cipher.organizationId)) ||
|
||||
((await this.keyService.getUserKeyWithLegacySupport(userId)) as UserKey)
|
||||
((await this.keyService.getUserKey(userId)) as UserKey)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1512,9 +1504,16 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
if (userCiphers.length === 0) {
|
||||
return encryptedCiphers;
|
||||
}
|
||||
|
||||
const useSdkEncryption = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM22136_SdkCipherEncryption,
|
||||
);
|
||||
|
||||
encryptedCiphers = await Promise.all(
|
||||
userCiphers.map(async (cipher) => {
|
||||
const encryptedCipher = await this.encrypt(cipher, userId, newUserKey, originalUserKey);
|
||||
const encryptedCipher = useSdkEncryption
|
||||
? await this.cipherEncryptionService.encryptCipherForRotation(cipher, userId, newUserKey)
|
||||
: await this.encrypt(cipher, userId, newUserKey, originalUserKey);
|
||||
return new CipherWithIdRequest(encryptedCipher);
|
||||
}),
|
||||
);
|
||||
@@ -1599,7 +1598,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
// In the case of a cipher that is being shared with an organization, we want to decrypt the
|
||||
// cipher key with the user's key and then re-encrypt it with the organization's key.
|
||||
private async encryptSharedCipher(model: CipherView, userId: UserId): Promise<EncryptionContext> {
|
||||
const keyForCipherKeyDecryption = await this.keyService.getUserKeyWithLegacySupport(userId);
|
||||
const keyForCipherKeyDecryption = await this.keyService.getUserKey(userId);
|
||||
return await this.encrypt(model, userId, null, keyForCipherKeyDecryption);
|
||||
}
|
||||
|
||||
@@ -1674,12 +1673,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id);
|
||||
const userKey = await this.keyService.getUserKey(activeUserId.id);
|
||||
const decBuf = await this.encryptService.decryptFileData(encBuf, userKey);
|
||||
|
||||
let encKey: UserKey | OrgKey;
|
||||
encKey = await this.keyService.getOrgKey(organizationId);
|
||||
encKey ||= (await this.keyService.getUserKeyWithLegacySupport()) as UserKey;
|
||||
encKey ||= (await this.keyService.getUserKey()) as UserKey;
|
||||
|
||||
const dataEncKey = await this.keyService.makeDataEncKey(encKey);
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { Fido2Credential } from "@bitwarden/common/vault/models/domain/fido2-credential";
|
||||
import {
|
||||
Fido2Credential as SdkFido2Credential,
|
||||
@@ -91,6 +94,7 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
vault: jest.fn().mockReturnValue({
|
||||
ciphers: jest.fn().mockReturnValue({
|
||||
encrypt: jest.fn(),
|
||||
encrypt_cipher_for_rotation: jest.fn(),
|
||||
set_fido2_credentials: jest.fn(),
|
||||
decrypt: jest.fn(),
|
||||
decrypt_list: jest.fn(),
|
||||
@@ -247,6 +251,31 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
cipher: sdkCipher,
|
||||
encryptedFor: userId,
|
||||
});
|
||||
|
||||
const newUserKey: UserKey = new SymmetricCryptoKey(
|
||||
Utils.fromUtf8ToArray("00000000000000000000000000000000"),
|
||||
) as UserKey;
|
||||
|
||||
const result = await cipherEncryptionService.encryptCipherForRotation(
|
||||
cipherViewObj,
|
||||
userId,
|
||||
newUserKey,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(mockSdkClient.vault().ciphers().encrypt_cipher_for_rotation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: cipherId }),
|
||||
newUserKey.toBase64(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveToOrganization", () => {
|
||||
it("should call the sdk method to move a cipher to an organization", async () => {
|
||||
const expectedCipher: Cipher = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EMPTY, catchError, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
CipherListView,
|
||||
@@ -84,6 +85,39 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
||||
);
|
||||
}
|
||||
|
||||
async encryptCipherForRotation(
|
||||
model: CipherView,
|
||||
userId: UserId,
|
||||
newKey: UserKey,
|
||||
): Promise<EncryptionContext | undefined> {
|
||||
return firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
map((sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
|
||||
using ref = sdk.take();
|
||||
const sdkCipherView = this.toSdkCipherView(model, ref.value);
|
||||
|
||||
const encryptionContext = ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.encrypt_cipher_for_rotation(sdkCipherView, newKey.toBase64());
|
||||
|
||||
return {
|
||||
cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
|
||||
encryptedFor: asUuid<UserId>(encryptionContext.encryptedFor),
|
||||
};
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to rotate cipher data: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async decrypt(cipher: Cipher, userId: UserId): Promise<CipherView> {
|
||||
return firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
|
||||
@@ -49,7 +49,6 @@ describe("Folder Service", () => {
|
||||
|
||||
keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any));
|
||||
encryptService.decryptString.mockResolvedValue("DEC");
|
||||
encryptService.decryptToUtf8.mockResolvedValue("DEC");
|
||||
|
||||
folderService = new FolderService(
|
||||
keyService,
|
||||
|
||||
Reference in New Issue
Block a user