1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-17900] Add cose / xchacha20poly1305 migration on userkey rotation (#14539)

* Add new encrypt service functions

* Undo changes

* Cleanup

* Fix build

* Fix comments

* Switch encrypt service to use SDK functions

* Add cose migration on userkey rotation

* Update sdk

* Set featureflag to default disabled

* Add tests

* Update sdk to build 168

* Make changes according to feedback
This commit is contained in:
Bernd Schoolmann
2025-05-20 21:25:14 +02:00
committed by GitHub
parent 7641dab0f0
commit d7c936e1ea
8 changed files with 133 additions and 27 deletions

View File

@@ -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<UserVerificationService>();
mockApiService = mock<UserKeyRotationApiService>();
mockCipherService = mock<CipherService>();
@@ -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);

View File

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

View File

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

View File

@@ -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,
};

View File

@@ -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.");
}

View File

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

8
package-lock.json generated
View File

@@ -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": {

View File

@@ -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",