Skip to content

Signing keys upgrade #14837

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6624d78
Add new encrypt service functions
quexten Apr 24, 2025
6c9fd9a
Undo changes
quexten Apr 24, 2025
fbc520b
Cleanup
quexten Apr 24, 2025
df18e7b
Merge branch 'main' into km/new-encrypt-service-interface
quexten Apr 25, 2025
b2ebf0f
Fix build
quexten Apr 25, 2025
c597a55
Fix comments
quexten Apr 28, 2025
69a10b2
Switch encrypt service to use SDK functions
quexten Apr 29, 2025
217af42
Add cose migration on userkey rotation
quexten Apr 29, 2025
77b1cd4
Merge branch 'main' into km/cose-upgrade
quexten May 8, 2025
a67cccb
Update sdk
quexten May 8, 2025
0b87ec6
Set featureflag to default disabled
quexten May 8, 2025
be966db
Merge branch 'main' into km/cose-upgrade
quexten May 8, 2025
bc426fc
Add tests
quexten May 9, 2025
58203e4
Merge branch 'km/cose-upgrade' of github.com:bitwarden/clients into kโ€ฆ
quexten May 9, 2025
d8b9698
Merge branch 'main' into km/cose-upgrade
quexten May 9, 2025
7b238e8
Update sdk to build 168
quexten May 12, 2025
fd0602d
Merge branch 'main' into km/cose-upgrade
quexten May 13, 2025
54a69aa
Signing keys upgrade
quexten May 19, 2025
9945481
Add get other user's keys endpoint
quexten May 19, 2025
4c3b640
Use sdk directly to get algorithm
quexten May 19, 2025
c524175
Cleanup
quexten May 19, 2025
43538c2
Add cleanup comment
quexten May 19, 2025
8461cad
Undo tsconfig changes
quexten May 19, 2025
cf8d4be
Implement sdk init with signing key
quexten May 19, 2025
3ea1dd1
Undo feature flag changes
quexten May 19, 2025
684f961
Clean up sync
quexten May 19, 2025
f37fb88
Add comments and add backward compatibilty for sync
quexten May 19, 2025
a04b207
Add more docs
quexten May 19, 2025
9128644
Implement clear key
quexten May 19, 2025
caede2e
Add tests
quexten May 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/web/src/app/key-management/debug/debug.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<ng-container>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This tool seems useful to keep around; it is compile-time enabled in dev-mode only, so there is no i18n or design input needed here.

The main purpose is to make debugging key management issues (getting keys from users, verifying them), and crypto state issues, easy to debug.

<form [formGroup]="formGroup">
<h1 class="box-header" id="settingsTitle">User Account</h1>
<div class="tw-flex tw-flex-col tw-space-y-3">UserId: {{ userId }}</div>
<h1>User Account Encryption</h1>
<div class="tw-flex tw-flex-col tw-space-y-3">
<h2>User Symmetric Key</h2>
Type: {{ userKeyType }} <br />
<input type="text" bitInput formControlName="userKey" placeholder="UserKey" />
<h2>User Asymmetric Encryption Keys</h2>
Type: {{ privateKeyType }} <br />
<input type="text" bitInput formControlName="privateKey" placeholder="privateKey" />
<input type="text" bitInput formControlName="publicKey" placeholder="publicKey" />
<h2>User Signing Key</h2>
<input type="text" bitInput formControlName="signingKey" placeholder="signingKey" />
<input type="text" bitInput formControlName="verifyingKey" placeholder="verifyingKey" />
</div>

<h1 class="box-header" id="settingsTitle">User Asymmetric Encryption</h1>
Tools to debug the asymmetric encryption PKI.
<div class="tw-flex tw-flex-col tw-space-y-3">
<h2>User Public Key</h2>
<input type="text" bitInput formControlName="fetchPKIUserId" placeholder="UserId" />
Result Public Key: {{ otherUserPublicKey }} Verifying Key:
{{ otherUserVerifyingKey }} PublicKeyOwnershipClaim: {{ otherUserPublicKeyOwnershipClaim }}
<button type="button" bitButton buttonType="primary" block (click)="getUserPublicKey()">
<span> Get Public Key </span>
</button>
</div>
<h1 class="box-header" id="settingsTitle">User Signing</h1>
Tools to debug user signing.
<div class="tw-flex tw-flex-col tw-space-y-3">
<h2>Public key ownership claim</h2>
Claim
<input
type="text"
bitInput
formControlName="testClaimPublicKeyOwnershipClaim"
placeholder="PublicKeyOwnershipClaim"
/>
PublicKey
<input type="text" bitInput formControlName="testClaimPublicKey" placeholder="PublicKey" />
VerifKey
<input
type="text"
bitInput
formControlName="testClaimVerifyingKey"
placeholder="VerifyingKey"
/>
Result: {{ testClaimPublicKeyOwnershipResult }}
<button
type="button"
bitButton
buttonType="primary"
block
(click)="verifyPublicKeyOwnershipClaim()"
>
<span> Verify Public Key Ownership Claim </span>
</button>
</div>
</form>
</ng-container>
110 changes: 110 additions & 0 deletions apps/web/src/app/key-management/debug/debug.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";

import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { KeyService } from "@bitwarden/key-management";
import { PureCrypto } from "@bitwarden/sdk-internal";

import { SharedModule } from "../../shared";

// The master key was originally used to encrypt user data, before the user key was introduced.
// This component is used to migrate from the old encryption scheme to the new one.
@Component({
standalone: true,
imports: [SharedModule],
templateUrl: "debug.component.html",
})
export class DebugMenu implements OnInit {
userKey: string;
userKeyType: string;

privateKey: string;
privateKeyType: string;

userId: string;

testClaimPublicKeyOwnershipResult: string;

formGroup: FormGroup = new FormGroup({
fetchPKIUserId: this.formBuilder.control("", [Validators.required]),
privateKey: this.formBuilder.control("", [Validators.required]),
publicKey: this.formBuilder.control("", [Validators.required]),
userKey: this.formBuilder.control("", [Validators.required]),
signingKey: this.formBuilder.control("", [Validators.required]),
verifyingKey: this.formBuilder.control("", [Validators.required]),

testClaimVerifyingKey: this.formBuilder.control("", [Validators.required]),
testClaimPublicKey: this.formBuilder.control("", [Validators.required]),
testClaimPublicKeyOwnershipClaim: this.formBuilder.control("", [Validators.required]),
});
otherUserPublicKey: string | null = null;
otherUserVerifyingKey: string | null = null;
otherUserPublicKeyOwnershipClaim: string | null = null;

constructor(
private keyService: KeyService,
private accountService: AccountService,
private formBuilder: FormBuilder,
private keyApiService: KeyApiService,
) {}

async ngOnInit() {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
this.userId = activeUserId.id;
const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId.id));
this.formGroup.get("userKey").setValue(userKey.keyB64);
this.userKeyType =
userKey.inner().type == EncryptionType.AesCbc256_HmacSha256_B64
? "AES-256-CBC-HMAC-SHA256"
: "COSE";

this.privateKey = Utils.fromBufferToB64(
(await firstValueFrom(this.keyService.userPrivateKey$(activeUserId.id))).buffer,
);
this.formGroup.get("privateKey").setValue(this.privateKey);
this.formGroup
.get("publicKey")
.setValue(
Utils.fromBufferToB64(
(await firstValueFrom(this.keyService.userPublicKey$(activeUserId.id))).buffer,
),
);
this.privateKeyType = "RSA";

const signingKey = await firstValueFrom(this.keyService.userSigningKey$(activeUserId.id));
const verifyingKey = Utils.fromBufferToB64(
PureCrypto.verifying_key_for_signing_key(signingKey.inner(), userKey.toEncoded()),
);
this.formGroup.get("signingKey").setValue(signingKey.inner());
this.formGroup.get("verifyingKey").setValue(verifyingKey);
}

getUserPublicKey = async () => {
const keys = await this.keyApiService.getUserPublicKeys(
this.formGroup.get("fetchPKIUserId").value,
);
this.otherUserPublicKey = keys.PublicKey;
this.otherUserVerifyingKey = keys.VerifyingKey.toString();
this.otherUserPublicKeyOwnershipClaim = keys.SignedPublicKeyOwnershipClaim.toString();
};

verifyPublicKeyOwnershipClaim = async () => {
const claim = this.formGroup.get("testClaimPublicKeyOwnershipClaim").value;
const publicKey = this.formGroup.get("testClaimPublicKey").value;
const verifyingKey = this.formGroup.get("testClaimVerifyingKey").value;
try {
const result = PureCrypto.verify_public_key_ownership_claim(
Utils.fromB64ToArray(claim),
Utils.fromB64ToArray(publicKey),
Utils.fromB64ToArray(verifyingKey),
);
this.testClaimPublicKeyOwnershipResult = result ? "Valid" : "Invalid";
} catch (e) {
this.testClaimPublicKeyOwnershipResult = "Error: " + e;
}
};
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { SigningKeyType } from "@bitwarden/key-management";

export class AccountKeysRequest {
// Other keys encrypted by the userkey
userKeyEncryptedAccountPrivateKey: string;
accountPublicKey: string;
signedPublicKeyOwnershipClaim: string | null;

userKeyEncryptedSigningKey: string | null;
verifyingKey: string | null;
signingKeyType: SigningKeyType | null;

constructor(userKeyEncryptedAccountPrivateKey: string, accountPublicKey: string) {
constructor(
userKeyEncryptedAccountPrivateKey: string,
accountPublicKey: string,
signedPublicKeyOwnershipClaim: string | null,
userKeyEncryptedSigningKey: string | null,
verifyingKey: string | null,
signingKeyType: SigningKeyType | null,
) {
this.userKeyEncryptedAccountPrivateKey = userKeyEncryptedAccountPrivateKey;
this.accountPublicKey = accountPublicKey;
this.signedPublicKeyOwnershipClaim = signedPublicKeyOwnershipClaim;
this.userKeyEncryptedSigningKey = userKeyEncryptedSigningKey;
this.verifyingKey = verifyingKey;
this.signingKeyType = signingKeyType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -158,6 +163,7 @@ describe("KeyRotationService", () => {
mockToastService,
mockI18nService,
mockDialogService,
mockConfigService,
);
});

Expand All @@ -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",
Expand Down Expand Up @@ -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).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 () => {
Expand Down Expand Up @@ -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);

Expand Down
Loading
Loading