diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts index dac5afa191..c65c4ac3ea 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.spec.ts @@ -11,7 +11,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -30,6 +29,7 @@ import { EmergencyAccessTrustComponent, KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; +import { PureCrypto } from "@bitwarden/sdk-internal"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth"; @@ -96,6 +96,11 @@ describe("KeyRotationService", () => { const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("test-public-key")]; beforeAll(() => { + jest.spyOn(PureCrypto, "make_user_key_aes256_cbc_hmac").mockReturnValue(new Uint8Array(64)); + jest.spyOn(PureCrypto, "make_user_key_xchacha20_poly1305").mockReturnValue(new Uint8Array(70)); + jest + .spyOn(PureCrypto, "encrypt_user_key_with_master_password") + .mockReturnValue("mockNewUserKey"); mockUserVerificationService = mock(); mockApiService = mock(); mockCipherService = mock(); @@ -158,6 +163,7 @@ describe("KeyRotationService", () => { mockToastService, mockI18nService, mockDialogService, + mockConfigService, ); }); @@ -181,7 +187,7 @@ describe("KeyRotationService", () => { } as any, ]); mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash"); - mockConfigService.getFeatureFlag.mockResolvedValue(true); + mockConfigService.getFeatureFlag.mockResolvedValue(false); mockEncryptService.wrapSymmetricKey.mockResolvedValue({ encryptedString: "mockEncryptedData", @@ -286,6 +292,59 @@ describe("KeyRotationService", () => { expect(arg.accountUnlockData.emergencyAccessUnlockData.length).toBe(1); expect(arg.accountUnlockData.organizationAccountRecoveryUnlockData.length).toBe(1); expect(arg.accountUnlockData.passkeyUnlockData.length).toBe(2); + expect(PureCrypto.make_user_key_aes256_cbc_hmac).toHaveBeenCalled(); + expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith( + new Uint8Array(64), + "newMasterPassword", + mockUser.email, + DEFAULT_KDF_CONFIG.toSdkConfig(), + ); + expect(PureCrypto.make_user_key_xchacha20_poly1305).not.toHaveBeenCalled(); + }); + + it("rotates the userkey to xchacha20poly1305 and encrypted data and changes master password when featureflag is active", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue; + EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted; + AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted; + await keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData( + "mockMasterPassword", + "newMasterPassword", + mockUser, + ); + + expect(mockApiService.postUserKeyUpdateV2).toHaveBeenCalled(); + const arg = mockApiService.postUserKeyUpdateV2.mock.calls[0][0]; + expect(arg.accountUnlockData.masterPasswordUnlockData.masterKeyEncryptedUserKey).toBe( + "mockNewUserKey", + ); + expect(arg.oldMasterKeyAuthenticationHash).toBe("mockMasterPasswordHash"); + expect(arg.accountUnlockData.masterPasswordUnlockData.email).toBe("mockEmail"); + expect(arg.accountUnlockData.masterPasswordUnlockData.kdfType).toBe( + DEFAULT_KDF_CONFIG.kdfType, + ); + expect(arg.accountUnlockData.masterPasswordUnlockData.kdfIterations).toBe( + DEFAULT_KDF_CONFIG.iterations, + ); + + expect(arg.accountKeys.accountPublicKey).toBe(Utils.fromUtf8ToB64("mockPublicKey")); + expect(arg.accountKeys.userKeyEncryptedAccountPrivateKey).toBe("mockEncryptedData"); + + expect(arg.accountData.ciphers.length).toBe(2); + expect(arg.accountData.folders.length).toBe(2); + expect(arg.accountData.sends.length).toBe(2); + expect(arg.accountUnlockData.emergencyAccessUnlockData.length).toBe(1); + expect(arg.accountUnlockData.organizationAccountRecoveryUnlockData.length).toBe(1); + expect(arg.accountUnlockData.passkeyUnlockData.length).toBe(2); + expect(PureCrypto.make_user_key_aes256_cbc_hmac).not.toHaveBeenCalled(); + expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith( + new Uint8Array(70), + "newMasterPassword", + mockUser.email, + DEFAULT_KDF_CONFIG.toSdkConfig(), + ); + expect(PureCrypto.make_user_key_xchacha20_poly1305).toHaveBeenCalled(); }); it("returns early when first trust warning dialog is declined", async () => { @@ -344,21 +403,6 @@ describe("KeyRotationService", () => { ).rejects.toThrow(); }); - it("throws if user key creation fails", async () => { - mockKeyService.makeUserKey.mockResolvedValueOnce([ - null as unknown as UserKey, - null as unknown as EncString, - ]); - - await expect( - keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData( - "mockMasterPassword", - "mockMasterPassword1", - mockUser, - ), - ).rejects.toThrow(); - }); - it("legacy throws if no private key is found", async () => { privateKey.next(null); diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index ec38f49bae..129d643f67 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -5,14 +5,17 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction"; import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; @@ -26,6 +29,7 @@ import { EmergencyAccessTrustComponent, KeyRotationTrustInfoComponent, } from "@bitwarden/key-management-ui"; +import { PureCrypto } from "@bitwarden/sdk-internal"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { WebauthnLoginAdminService } from "../../auth/core"; @@ -59,6 +63,7 @@ export class UserKeyRotationService { private toastService: ToastService, private i18nService: I18nService, private dialogService: DialogService, + private configService: ConfigService, ) {} /** @@ -116,8 +121,22 @@ export class UserKeyRotationService { const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig); - const [newUnencryptedUserKey, newMasterKeyEncryptedUserKey] = - await this.keyService.makeUserKey(newMasterKey); + let userKeyBytes: Uint8Array; + if (await this.configService.getFeatureFlag(FeatureFlag.EnrollAeadOnKeyRotation)) { + userKeyBytes = PureCrypto.make_user_key_xchacha20_poly1305(); + } else { + userKeyBytes = PureCrypto.make_user_key_aes256_cbc_hmac(); + } + + const newMasterKeyEncryptedUserKey = new EncString( + PureCrypto.encrypt_user_key_with_master_password( + userKeyBytes, + newMasterPassword, + email, + kdfConfig.toSdkConfig(), + ), + ); + const newUnencryptedUserKey = new SymmetricCryptoKey(userKeyBytes) as UserKey; if (!newUnencryptedUserKey || !newMasterKeyEncryptedUserKey) { this.logService.info("[Userkey rotation] User key could not be created. Aborting!"); diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index c71215a4a0..f9e68efe4b 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -48,6 +48,7 @@ export enum FeatureFlag { PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", UseSDKForDecryption = "use-sdk-for-decryption", PM17987_BlockType0 = "pm-17987-block-type-0", + EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation", /* Tools */ ItemShare = "item-share", @@ -131,6 +132,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, [FeatureFlag.UseSDKForDecryption]: FALSE, [FeatureFlag.PM17987_BlockType0]: FALSE, + [FeatureFlag.EnrollAeadOnKeyRotation]: FALSE, /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE, diff --git a/libs/common/src/platform/enums/encryption-type.enum.ts b/libs/common/src/platform/enums/encryption-type.enum.ts index 7f4b5048a4..426b1a2313 100644 --- a/libs/common/src/platform/enums/encryption-type.enum.ts +++ b/libs/common/src/platform/enums/encryption-type.enum.ts @@ -1,9 +1,16 @@ // FIXME: update to use a const object instead of a typescript enum // eslint-disable-next-line @bitwarden/platform/no-enums export enum EncryptionType { + // Symmetric encryption types AesCbc256_B64 = 0, // Type 1 was the unused and removed AesCbc128_HmacSha256_B64 AesCbc256_HmacSha256_B64 = 2, + // Cose is the encoding for the key used, but contained can be: + // - XChaCha20Poly1305 + CoseEncrypt0 = 7, + + // Asymmetric encryption types. These never occur in the same places that the symmetric ones would + // and can be split out into a separate enum. Rsa2048_OaepSha256_B64 = 3, Rsa2048_OaepSha1_B64 = 4, Rsa2048_OaepSha256_HmacSha256_B64 = 5, @@ -38,4 +45,5 @@ export const EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE = { [EncryptionType.Rsa2048_OaepSha1_B64]: 1, [EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64]: 2, [EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64]: 2, + [EncryptionType.CoseEncrypt0]: 1, }; diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index ad16ddd06f..1fdca04ace 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -16,13 +16,19 @@ export type Aes256CbcKey = { encryptionKey: Uint8Array; }; +export type CoseKey = { + type: EncryptionType.CoseEncrypt0; + // Encryption key here refers to the cose-encoded and padded key. This MAY later be refactored to contain the actual key bytes, as is the case in the SDK + encryptionKey: Uint8Array; +}; + /** * A symmetric crypto key represents a symmetric key usable for symmetric encryption and decryption operations. * The specific algorithm used is private to the key, and should only be exposed to encrypt service implementations. * This can be done via `inner()`. */ export class SymmetricCryptoKey { - private innerKey: Aes256CbcHmacKey | Aes256CbcKey; + private innerKey: Aes256CbcHmacKey | Aes256CbcKey | CoseKey; keyB64: string; @@ -47,6 +53,12 @@ export class SymmetricCryptoKey { authenticationKey: key.slice(32), }; this.keyB64 = this.toBase64(); + } else if (key.byteLength > 64) { + this.innerKey = { + type: EncryptionType.CoseEncrypt0, + encryptionKey: key, + }; + this.keyB64 = this.toBase64(); } else { throw new Error(`Unsupported encType/key length ${key.byteLength}`); } @@ -63,7 +75,7 @@ export class SymmetricCryptoKey { * * @returns The inner key instance that can be directly used for encryption primitives */ - inner(): Aes256CbcHmacKey | Aes256CbcKey { + inner(): Aes256CbcHmacKey | Aes256CbcKey | CoseKey { return this.innerKey; } @@ -90,6 +102,8 @@ export class SymmetricCryptoKey { encodedKey.set(this.innerKey.encryptionKey, 0); encodedKey.set(this.innerKey.authenticationKey, 32); return encodedKey; + } else if (this.innerKey.type === EncryptionType.CoseEncrypt0) { + return this.innerKey.encryptionKey; } else { throw new Error("Unsupported encryption type."); } diff --git a/libs/key-management/src/models/kdf-config.ts b/libs/key-management/src/models/kdf-config.ts index 689da77c16..a2ed8a2250 100644 --- a/libs/key-management/src/models/kdf-config.ts +++ b/libs/key-management/src/models/kdf-config.ts @@ -1,6 +1,7 @@ import { Jsonify } from "type-fest"; import { RangeWithDefault } from "@bitwarden/common/platform/misc/range-with-default"; +import { Kdf } from "@bitwarden/sdk-internal"; import { KdfType } from "../enums/kdf-type.enum"; @@ -49,6 +50,14 @@ export class PBKDF2KdfConfig { static fromJSON(json: Jsonify): PBKDF2KdfConfig { return new PBKDF2KdfConfig(json.iterations); } + + toSdkConfig(): Kdf { + return { + pBKDF2: { + iterations: this.iterations, + }, + }; + } } /** @@ -124,6 +133,16 @@ export class Argon2KdfConfig { static fromJSON(json: Jsonify): Argon2KdfConfig { return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism); } + + toSdkConfig(): Kdf { + return { + argon2id: { + iterations: this.iterations, + memory: this.memory, + parallelism: this.parallelism, + }, + }; + } } export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2KdfConfig.ITERATIONS.defaultValue); diff --git a/package-lock.json b/package-lock.json index 9707005d89..bd2cabe1cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.159", + "@bitwarden/sdk-internal": "0.2.0-main.168", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.1.0", @@ -4830,9 +4830,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.159", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.159.tgz", - "integrity": "sha512-vliX5w/A6fuKWZJpDZTCPV4EU5CFrrs6zAv0aQaUQXF9LqL1YVh113D1NhOMuG2ILLWs2kDcTKiprvWFSTu1dg==", + "version": "0.2.0-main.168", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.168.tgz", + "integrity": "sha512-NU10oqw+GI9oHrh8/i/IC8/7oaYmswqC2E/0Zc56xC3jY7uNgFZgpae7JhyMU6UxzrAjiEqdmGnm+AGWFiPG8w==", "license": "GPL-3.0" }, "node_modules/@bitwarden/send-ui": { diff --git a/package.json b/package.json index fc948fc5ee..9831417de5 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.159", + "@bitwarden/sdk-internal": "0.2.0-main.168", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.1.0",