1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +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

@@ -0,0 +1,55 @@
import { SecurityStateResponse } from "../../security-state/response/security-state.response";
import { PublicKeyEncryptionKeyPairResponse } from "./public-key-encryption-key-pair.response";
import { SignatureKeyPairResponse } from "./signature-key-pair.response";
/**
* The privately accessible view of an entity (account / org)'s keys.
* This includes the full key-pairs for public-key encryption and signing, as well as the security state if available.
*/
export class PrivateKeysResponseModel {
readonly publicKeyEncryptionKeyPair: PublicKeyEncryptionKeyPairResponse;
readonly signatureKeyPair: SignatureKeyPairResponse | null = null;
readonly securityState: SecurityStateResponse | null = null;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (
!("publicKeyEncryptionKeyPair" in response) ||
typeof response.publicKeyEncryptionKeyPair !== "object"
) {
throw new TypeError("Response must contain a valid publicKeyEncryptionKeyPair");
}
this.publicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairResponse(
response.publicKeyEncryptionKeyPair,
);
if (
"signatureKeyPair" in response &&
typeof response.signatureKeyPair === "object" &&
response.signatureKeyPair != null
) {
this.signatureKeyPair = new SignatureKeyPairResponse(response.signatureKeyPair);
}
if (
"securityState" in response &&
typeof response.securityState === "object" &&
response.securityState != null
) {
this.securityState = new SecurityStateResponse(response.securityState);
}
if (
(this.signatureKeyPair !== null && this.securityState === null) ||
(this.signatureKeyPair === null && this.securityState !== null)
) {
throw new TypeError(
"Both signatureKeyPair and securityState must be present or absent together",
);
}
}
}

View File

@@ -0,0 +1,32 @@
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SignedPublicKey, UnsignedPublicKey, WrappedPrivateKey } from "../../types";
export class PublicKeyEncryptionKeyPairResponse {
readonly wrappedPrivateKey: WrappedPrivateKey;
readonly publicKey: UnsignedPublicKey;
readonly signedPublicKey: SignedPublicKey | null = null;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (!("publicKey" in response) || typeof response.publicKey !== "string") {
throw new TypeError("Response must contain a valid publicKey");
}
this.publicKey = Utils.fromB64ToArray(response.publicKey) as UnsignedPublicKey;
if (!("wrappedPrivateKey" in response) || typeof response.wrappedPrivateKey !== "string") {
throw new TypeError("Response must contain a valid wrappedPrivateKey");
}
this.wrappedPrivateKey = response.wrappedPrivateKey as WrappedPrivateKey;
if ("signedPublicKey" in response && typeof response.signedPublicKey === "string") {
this.signedPublicKey = response.signedPublicKey as SignedPublicKey;
} else {
this.signedPublicKey = null;
}
}
}

View File

@@ -0,0 +1,44 @@
import { SignedPublicKey } from "@bitwarden/sdk-internal";
import { UnsignedPublicKey, VerifyingKey } from "../../types";
/**
* The publicly accessible view of an entity (account / org)'s keys. That includes the encryption public key, and the verifying key if available.
*/
export class PublicKeysResponseModel {
readonly publicKey: UnsignedPublicKey;
readonly verifyingKey: VerifyingKey | null;
readonly signedPublicKey?: SignedPublicKey | null;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (!("publicKey" in response) || !(response.publicKey instanceof Uint8Array)) {
throw new TypeError("Response must contain a valid publicKey");
}
this.publicKey = response.publicKey as UnsignedPublicKey;
if ("verifyingKey" in response && typeof response.verifyingKey === "string") {
this.verifyingKey = response.verifyingKey as VerifyingKey;
} else {
this.verifyingKey = null;
}
if ("signedPublicKey" in response && typeof response.signedPublicKey === "string") {
this.signedPublicKey = response.signedPublicKey as SignedPublicKey;
} else {
this.signedPublicKey = null;
}
if (
(this.signedPublicKey !== null && this.verifyingKey === null) ||
(this.signedPublicKey === null && this.verifyingKey !== null)
) {
throw new TypeError(
"Both signedPublicKey and verifyingKey must be present or absent together",
);
}
}
}

View File

@@ -0,0 +1,22 @@
import { VerifyingKey, WrappedSigningKey } from "../../types";
export class SignatureKeyPairResponse {
readonly wrappedSigningKey: WrappedSigningKey;
readonly verifyingKey: VerifyingKey;
constructor(response: unknown) {
if (typeof response !== "object" || response == null) {
throw new TypeError("Response must be an object");
}
if (!("wrappedSigningKey" in response) || typeof response.wrappedSigningKey !== "string") {
throw new TypeError("Response must contain a valid wrappedSigningKey");
}
this.wrappedSigningKey = response.wrappedSigningKey as WrappedSigningKey;
if (!("verifyingKey" in response) || typeof response.verifyingKey !== "string") {
throw new TypeError("Response must contain a valid verifyingKey");
}
this.verifyingKey = response.verifyingKey as VerifyingKey;
}
}

View File

@@ -0,0 +1,5 @@
import { PublicKeysResponseModel } from "../../response/public-keys.response";
export abstract class KeyApiService {
abstract getUserPublicKeys(id: string): Promise<PublicKeysResponseModel>;
}

View File

@@ -0,0 +1,15 @@
import { UserId } from "@bitwarden/common/types/guid";
import { ApiService } from "../../../abstractions/api.service";
import { PublicKeysResponseModel } from "../response/public-keys.response";
import { KeyApiService } from "./abstractions/key-api-service.abstraction";
export class DefaultKeyApiService implements KeyApiService {
constructor(private apiService: ApiService) {}
async getUserPublicKeys(id: UserId): Promise<PublicKeysResponseModel> {
const response = await this.apiService.send("GET", "/users/" + id + "/keys", null, true, true);
return new PublicKeysResponseModel(response);
}
}