mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 10:13:31 +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:
@@ -8,6 +8,7 @@ import {
|
||||
} from "../../../platform/models/domain/decrypt-parameters";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../../types/csprng";
|
||||
import { UnsignedPublicKey } from "../../types";
|
||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||
|
||||
export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
@@ -309,7 +310,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
"encrypt",
|
||||
]);
|
||||
const buffer = await this.subtle.exportKey("spki", impPublicKey);
|
||||
return new Uint8Array(buffer);
|
||||
return new Uint8Array(buffer) as UnsignedPublicKey;
|
||||
}
|
||||
|
||||
async aesGenerateKey(bitLength = 128 | 192 | 256 | 512): Promise<CsprngArray> {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const SigningKeyTypes = {
|
||||
Ed25519: "ed25519",
|
||||
} as const;
|
||||
|
||||
export type SigningKeyType = (typeof SigningKeyTypes)[keyof typeof SigningKeyTypes];
|
||||
export function parseSigningKeyTypeFromString(value: string): SigningKeyType {
|
||||
switch (value) {
|
||||
case SigningKeyTypes.Ed25519:
|
||||
return SigningKeyTypes.Ed25519;
|
||||
default:
|
||||
throw new Error(`Unknown signing key type: ${value}`);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PublicKeysResponseModel } from "../../response/public-keys.response";
|
||||
|
||||
export abstract class KeyApiService {
|
||||
abstract getUserPublicKeys(id: string): Promise<PublicKeysResponseModel>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { SignedSecurityState } from "../../types";
|
||||
|
||||
export abstract class SecurityStateService {
|
||||
/**
|
||||
* Retrieves the security state for the provided user.
|
||||
* Note: This state is not yet validated. To get a validated state, the SDK crypto client
|
||||
* must be used. This security state is validated on initialization of the SDK.
|
||||
*/
|
||||
abstract accountSecurityState$(userId: UserId): Observable<SignedSecurityState | null>;
|
||||
/**
|
||||
* Sets the security state for the provided user.
|
||||
*/
|
||||
abstract setAccountSecurityState(
|
||||
accountSecurityState: SignedSecurityState,
|
||||
userId: UserId,
|
||||
): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { SignedSecurityState } from "../../types";
|
||||
|
||||
export class SecurityStateRequest {
|
||||
constructor(
|
||||
readonly securityState: SignedSecurityState,
|
||||
readonly securityVersion: number,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { SignedSecurityState } from "../../types";
|
||||
|
||||
export class SecurityStateResponse {
|
||||
readonly securityState: SignedSecurityState | null = null;
|
||||
|
||||
constructor(response: unknown) {
|
||||
if (typeof response !== "object" || response == null) {
|
||||
throw new TypeError("Response must be an object");
|
||||
}
|
||||
|
||||
if (!("securityState" in response) || !(typeof response.securityState === "string")) {
|
||||
throw new TypeError("Response must contain a valid securityState");
|
||||
}
|
||||
this.securityState = response.securityState as SignedSecurityState;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { SignedSecurityState } from "../../types";
|
||||
import { SecurityStateService } from "../abstractions/security-state.service";
|
||||
import { ACCOUNT_SECURITY_STATE } from "../state/security-state.state";
|
||||
|
||||
export class DefaultSecurityStateService implements SecurityStateService {
|
||||
constructor(protected stateProvider: StateProvider) {}
|
||||
|
||||
// Emits the provided user's security state, or null if there is no security state present for the user.
|
||||
accountSecurityState$(userId: UserId): Observable<SignedSecurityState | null> {
|
||||
return this.stateProvider.getUserState$(ACCOUNT_SECURITY_STATE, userId);
|
||||
}
|
||||
|
||||
// Sets the security state for the provided user.
|
||||
// This is not yet validated, and is only validated upon SDK initialization.
|
||||
async setAccountSecurityState(
|
||||
accountSecurityState: SignedSecurityState,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.stateProvider.setUserState(ACCOUNT_SECURITY_STATE, accountSecurityState, userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CRYPTO_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { SignedSecurityState } from "../../types";
|
||||
|
||||
export const ACCOUNT_SECURITY_STATE = new UserKeyDefinition<SignedSecurityState>(
|
||||
CRYPTO_DISK,
|
||||
"accountSecurityState",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
30
libs/common/src/key-management/types.ts
Normal file
30
libs/common/src/key-management/types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
import { EncString, SignedSecurityState as SdkSignedSecurityState } from "@bitwarden/sdk-internal";
|
||||
|
||||
/**
|
||||
* A private key, encrypted with a symmetric key.
|
||||
*/
|
||||
export type WrappedPrivateKey = Opaque<EncString, "WrappedPrivateKey">;
|
||||
|
||||
/**
|
||||
* A public key, signed with the accounts signature key.
|
||||
*/
|
||||
export type SignedPublicKey = Opaque<string, "SignedPublicKey">;
|
||||
/**
|
||||
* A public key in base64 encoded SPKI-DER
|
||||
*/
|
||||
export type UnsignedPublicKey = Opaque<Uint8Array, "UnsignedPublicKey">;
|
||||
|
||||
/**
|
||||
* A signature key encrypted with a symmetric key.
|
||||
*/
|
||||
export type WrappedSigningKey = Opaque<EncString, "WrappedSigningKey">;
|
||||
/**
|
||||
* A signature public key (verifying key) in base64 encoded CoseKey format
|
||||
*/
|
||||
export type VerifyingKey = Opaque<string, "VerifyingKey">;
|
||||
/**
|
||||
* A signed security state, encoded in base64.
|
||||
*/
|
||||
export type SignedSecurityState = Opaque<SdkSignedSecurityState, "SignedSecurityState">;
|
||||
Reference in New Issue
Block a user