1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[PM-21033/PM-22863] User Encryption v2 (#14942)

* Add new encrypt service functions

* Undo changes

* Cleanup

* Fix build

* Fix comments

* Switch encrypt service to use SDK functions

* Move remaining functions to PureCrypto

* Tests

* Increase test coverage

* Split up userkey rotation v2 and add tests

* Fix eslint

* Fix type errors

* Fix tests

* Implement signing keys

* Fix sdk init

* Remove key rotation v2 flag

* Fix parsing when user does not have signing keys

* Clear up trusted key naming

* Split up getNewAccountKeys

* Add trim and lowercase

* Replace user.email with masterKeySalt

* Add wasTrustDenied to verifyTrust in key rotation service

* Move testable userkey rotation service code to testable class

* Fix build

* Add comments

* Undo changes

* Fix incorrect behavior on aborting key rotation and fix import

* Fix tests

* Make members of userkey rotation service protected

* Fix type error

* Cleanup and add injectable annotation

* Fix tests

* Update apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove v1 rotation request

* Add upgrade to user encryption v2

* Fix types

* Update sdk method calls

* Update request models for new server api for rotation

* Fix build

* Update userkey rotation for new server API

* Update crypto client call for new sdk changes

* Fix rotation with signing keys

* Cargo lock

* Fix userkey rotation service

* Fix types

* Undo changes to feature flag service

* Fix linting

* [PM-22863] Account security state (#15309)

* Add account security state

* Update key rotation

* Rename

* Fix build

* Cleanup

* Further cleanup

* Tests

* Increase test coverage

* Add test

* Increase test coverage

* Fix builds and update sdk

* Fix build

* Fix tests

* Reset changes to encrypt service

* Cleanup

* Add comment

* Cleanup

* Cleanup

* Rename model

* Cleanup

* Fix build

* Clean up

* Fix types

* Cleanup

* Cleanup

* Cleanup

* Add test

* Simplify request model

* Rename and add comments

* Fix tests

* Update responses to use less strict typing

* Fix response parsing for v1 users

* Update libs/common/src/key-management/keys/response/private-keys.response.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update libs/common/src/key-management/keys/response/private-keys.response.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Fix build

* Fix build

* Fix build

* Undo change

* Fix attachments not encrypting for v2 users

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2025-10-10 23:04:47 +02:00
committed by GitHub
parent 89eb60135f
commit cc8bd71775
36 changed files with 1693 additions and 327 deletions

View File

@@ -7,6 +7,7 @@ import {
EncryptedString,
EncString,
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { WrappedSigningKey } from "@bitwarden/common/key-management/types";
import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
@@ -236,8 +237,10 @@ export abstract class KeyService {
*/
abstract getOrgKey(orgId: string): Promise<OrgKey | null>;
/**
* Uses the org key to derive a new symmetric key for encrypting data
* @param key The organization's symmetric key
* Makes a fresh attachment content encryption key and returns it along with a wrapped (encrypted) version of it.
* @deprecated Do not use this for new code / new cryptographic designs.
* @param key The organization's symmetric key or the user's user key to wrap the attachment key with
* @returns The new attachment content encryption key and the wrapped version of it
*/
abstract makeDataEncKey<T extends UserKey | OrgKey>(
key: T,
@@ -272,6 +275,14 @@ export abstract class KeyService {
* @param encPrivateKey An encrypted private key
*/
abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Sets the user's encrypted signing key in storage
* In contrast to the private key, the decrypted signing key
* is not stored in memory outside of the SDK.
* @param encryptedSigningKey An encrypted signing key
* @param userId The user id of the user to set the signing key for
*/
abstract setUserSigningKey(encryptedSigningKey: WrappedSigningKey, userId: UserId): Promise<void>;
/**
* Gets an observable stream of the given users decrypted private key, will emit null if the user
@@ -416,7 +427,13 @@ export abstract class KeyService {
*
* @throws If an invalid user id is passed in.
*/
abstract userPublicKey$(userId: UserId): Observable<UserPublicKey | null>;
abstract userPublicKey$(userId: UserId): Observable<Uint8Array | null>;
/**
* Gets a users signing keys from local state.
* The observable will emit null, exactly if the local state returns null.
*/
abstract userSigningKey$(userId: UserId): Observable<WrappedSigningKey | null>;
/**
* Validates that a userkey is correct for a given user

View File

@@ -11,6 +11,7 @@ import {
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { UnsignedPublicKey, WrappedSigningKey } from "@bitwarden/common/key-management/types";
import { VaultTimeoutStringType } from "@bitwarden/common/key-management/vault-timeout";
import { VAULT_TIMEOUT } from "@bitwarden/common/key-management/vault-timeout/services/vault-timeout-settings.state";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -25,6 +26,7 @@ import {
USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY,
USER_KEY,
USER_KEY_ENCRYPTED_SIGNING_KEY,
} from "@bitwarden/common/platform/services/key-state/user-key.state";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import {
@@ -432,6 +434,7 @@ describe("keyService", () => {
USER_ENCRYPTED_ORGANIZATION_KEYS,
USER_ENCRYPTED_PROVIDER_KEYS,
USER_ENCRYPTED_PRIVATE_KEY,
USER_KEY_ENCRYPTED_SIGNING_KEY,
USER_KEY,
])("key removal", (key: UserKeyDefinition<unknown>) => {
it(`clears ${key.key} for the specified user when specified`, async () => {
@@ -540,6 +543,51 @@ describe("keyService", () => {
});
});
describe("userSigningKey$", () => {
it("returns the signing key when the user has a signing key set", async () => {
const fakeSigningKey = "" as WrappedSigningKey;
const fakeSigningKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_KEY_ENCRYPTED_SIGNING_KEY,
);
fakeSigningKeyState.nextState(fakeSigningKey);
const signingKey = await firstValueFrom(keyService.userSigningKey$(mockUserId));
expect(signingKey).toEqual(fakeSigningKey);
});
it("returns null when the user does not have a signing key set", async () => {
const signingKey = await firstValueFrom(keyService.userSigningKey$(mockUserId));
expect(signingKey).toBeFalsy();
});
});
describe("setUserSigningKey", () => {
it("throws if the signing key is null", async () => {
await expect(keyService.setUserSigningKey(null as any, mockUserId)).rejects.toThrow(
"No user signing key provided.",
);
});
it("throws if the userId is null", async () => {
await expect(
keyService.setUserSigningKey("" as WrappedSigningKey, null as unknown as UserId),
).rejects.toThrow("No userId provided.");
});
it("sets the signing key for the user", async () => {
const fakeSigningKey = "" as WrappedSigningKey;
const fakeSigningKeyState = stateProvider.singleUser.getFake(
mockUserId,
USER_KEY_ENCRYPTED_SIGNING_KEY,
);
fakeSigningKeyState.nextState(null);
await keyService.setUserSigningKey(fakeSigningKey, mockUserId);
expect(fakeSigningKeyState.nextMock).toHaveBeenCalledTimes(1);
expect(fakeSigningKeyState.nextMock).toHaveBeenCalledWith(fakeSigningKey);
});
});
describe("cipherDecryptionKeys$", () => {
function fakePrivateKeyDecryption(encryptedPrivateKey: EncString, key: SymmetricCryptoKey) {
const output = new Uint8Array(64);
@@ -1132,12 +1180,12 @@ describe("keyService", () => {
keyService.userPrivateKey$ = jest.fn().mockReturnValue(new BehaviorSubject("private key"));
cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(
Utils.fromUtf8ToArray("public key"),
Utils.fromUtf8ToArray("public key") as UnsignedPublicKey,
);
const key = await firstValueFrom(keyService.userEncryptionKeyPair$(mockUserId));
expect(key).toEqual({
privateKey: "private key",
publicKey: Utils.fromUtf8ToArray("public key"),
publicKey: Utils.fromUtf8ToArray("public key") as UnsignedPublicKey,
});
});
});

View File

@@ -28,6 +28,7 @@ import {
} from "@bitwarden/common/key-management/crypto/models/enc-string";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { WrappedSigningKey } from "@bitwarden/common/key-management/types";
import { VaultTimeoutStringType } from "@bitwarden/common/key-management/vault-timeout";
import { VAULT_TIMEOUT } from "@bitwarden/common/key-management/vault-timeout/services/vault-timeout-settings.state";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -44,6 +45,7 @@ import {
USER_ENCRYPTED_PRIVATE_KEY,
USER_EVER_HAD_USER_KEY,
USER_KEY,
USER_KEY_ENCRYPTED_SIGNING_KEY,
} from "@bitwarden/common/platform/services/key-state/user-key.state";
import { StateProvider } from "@bitwarden/common/platform/state";
import { CsprngArray } from "@bitwarden/common/types/csprng";
@@ -398,8 +400,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
throw new Error("No key provided");
}
const newSymKey = await this.keyGenerationService.createKey(512);
return this.buildProtectedSymmetricKey(key, newSymKey);
// Content encryption key is AES256_CBC_HMAC
const cek = await this.keyGenerationService.createKey(512);
const wrappedCek = await this.encryptService.wrapSymmetricKey(cek, key);
return [cek, wrappedCek];
}
private async clearOrgKeys(userId: UserId): Promise<void> {
@@ -505,6 +509,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
}
private async clearSigningKey(userId: UserId): Promise<void> {
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_SIGNING_KEY, null, userId);
}
async clearPinKeys(userId: UserId): Promise<void> {
if (userId == null) {
throw new Error("UserId is required");
@@ -537,6 +545,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
await this.clearOrgKeys(userId);
await this.clearProviderKeys(userId);
await this.clearKeyPair(userId);
await this.clearSigningKey(userId);
await this.clearPinKeys(userId);
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
}
@@ -758,6 +767,10 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return phrase;
}
/**
* @deprecated
* This should only be used for wrapping the user key with a master key or stretched master key.
*/
private async buildProtectedSymmetricKey<T extends SymmetricCryptoKey>(
encryptionKey: SymmetricCryptoKey,
newSymKey: SymmetricCryptoKey,
@@ -792,7 +805,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return null;
}
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
return await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
}
userPrivateKey$(userId: UserId): Observable<UserPrivateKey | null> {
@@ -808,7 +821,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
return null;
}
const publicKey = (await this.derivePublicKey(privateKey))!;
const publicKey = (await this.derivePublicKey(privateKey))! as UserPublicKey;
return { privateKey, publicKey };
}),
);
@@ -905,6 +918,27 @@ export class DefaultKeyService implements KeyServiceAbstraction {
);
}
async setUserSigningKey(userSigningKey: WrappedSigningKey, userId: UserId): Promise<void> {
if (userSigningKey == null) {
throw new Error("No user signing key provided.");
}
if (userId == null) {
throw new Error("No userId provided.");
}
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_SIGNING_KEY, userSigningKey, userId);
}
userSigningKey$(userId: UserId): Observable<WrappedSigningKey | null> {
return this.stateProvider.getUser(userId, USER_KEY_ENCRYPTED_SIGNING_KEY).state$.pipe(
map((encryptedSigningKey) => {
if (encryptedSigningKey == null) {
return null;
}
return encryptedSigningKey as WrappedSigningKey;
}),
);
}
orgKeys$(userId: UserId): Observable<Record<OrganizationId, OrgKey> | null> {
return this.cipherDecryptionKeys$(userId).pipe(map((keys) => keys?.orgKeys ?? null));
}