diff --git a/libs/state/src/state-migrations/migrations/75-remove-user-encrypted-private-key.spec.ts b/libs/state/src/state-migrations/migrations/75-remove-user-encrypted-private-key.spec.ts index 8fd31b9ed47..7c3b765dbd8 100644 --- a/libs/state/src/state-migrations/migrations/75-remove-user-encrypted-private-key.spec.ts +++ b/libs/state/src/state-migrations/migrations/75-remove-user-encrypted-private-key.spec.ts @@ -7,7 +7,69 @@ describe("RemoveUserEncryptedPrivateKey", () => { const sut = new RemoveUserEncryptedPrivateKey(74, 75); describe("migrate", () => { - it("deletes user encrypted private key, signing key, and signed public key from all users", async () => { + it("migrates V1 cryptographic state (privateKey only)", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + }, + user_user1_crypto_privateKey: "encryptedPrivateKey", + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + }, + user_user1_crypto_accountCryptographicState: { + V1: { + private_key: "encryptedPrivateKey", + }, + }, + }); + }); + + it("migrates V2 cryptographic state (all keys present)", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + }, + user_user1_crypto_privateKey: "encryptedPrivateKey", + user_user1_crypto_userSigningKey: "signingKey", + user_user1_crypto_userSignedPublicKey: "signedPublicKey", + user_user1_crypto_accountSecurityState: "securityState", + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + }, + user_user1_crypto_accountCryptographicState: { + V2: { + private_key: "encryptedPrivateKey", + signing_key: "signingKey", + signed_public_key: "signedPublicKey", + security_state: "securityState", + }, + }, + }); + }); + + it("migrates multiple users with different cryptographic states", async () => { const output = await runMigrator(sut, { global_account_accounts: { user1: { @@ -20,15 +82,20 @@ describe("RemoveUserEncryptedPrivateKey", () => { name: "User 2", emailVerified: true, }, + user3: { + email: "user3@email.com", + name: "User 3", + emailVerified: true, + }, }, - user_user1_CRYPTO_DISK_privateKey: "abc", - user_user2_CRYPTO_DISK_privateKey: "def", - user_user1_CRYPTO_DISK_userSigningKey: "sign1", - user_user2_CRYPTO_DISK_userSigningKey: "sign2", - user_user1_CRYPTO_DISK_userSignedPublicKey: "pub1", - user_user2_CRYPTO_DISK_userSignedPublicKey: "pub2", - user_user1_CRYPTO_DISK_accountSecurityState: "security1", - user_user2_CRYPTO_DISK_accountSecurityState: "security2", + // user1: V1 state + user_user1_crypto_privateKey: "privateKey1", + // user2: V2 state + user_user2_crypto_privateKey: "privateKey2", + user_user2_crypto_userSigningKey: "signingKey2", + user_user2_crypto_userSignedPublicKey: "signedPublicKey2", + user_user2_crypto_accountSecurityState: "securityState2", + // user3: no cryptographic state }); expect(output).toEqual({ @@ -43,7 +110,56 @@ describe("RemoveUserEncryptedPrivateKey", () => { name: "User 2", emailVerified: true, }, + user3: { + email: "user3@email.com", + name: "User 3", + emailVerified: true, + }, }, + user_user1_crypto_accountCryptographicState: { + V1: { + private_key: "privateKey1", + }, + }, + user_user2_crypto_accountCryptographicState: { + V2: { + private_key: "privateKey2", + signing_key: "signingKey2", + signed_public_key: "signedPublicKey2", + security_state: "securityState2", + }, + }, + }); + }); + + it("does not overwrite existing accountCryptographicState", async () => { + const existingState = { + V1: { + private_key: "existingPrivateKey", + }, + }; + + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + }, + user_user1_crypto_accountCryptographicState: existingState, + user_user1_crypto_privateKey: "newPrivateKey", + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + }, + user_user1_crypto_accountCryptographicState: existingState, }); }); }); diff --git a/libs/state/src/state-migrations/migrations/75-remove-user-encrypted-private-key.ts b/libs/state/src/state-migrations/migrations/75-remove-user-encrypted-private-key.ts index b6c63479219..82cd0582f74 100644 --- a/libs/state/src/state-migrations/migrations/75-remove-user-encrypted-private-key.ts +++ b/libs/state/src/state-migrations/migrations/75-remove-user-encrypted-private-key.ts @@ -1,62 +1,95 @@ import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; import { IRREVERSIBLE, Migrator } from "../migrator"; +import { SignedPublicKey, +WrappedAccountCryptographicState, EncString, SignedSecurityState } from "@bitwarden/sdk-internal"; type ExpectedAccountType = NonNullable; export const userEncryptedPrivateKey: KeyDefinitionLike = { key: "privateKey", stateDefinition: { - name: "CRYPTO_DISK", + name: "crypto", }, }; export const userKeyEncryptedSigningKey: KeyDefinitionLike = { key: "userSigningKey", stateDefinition: { - name: "CRYPTO_DISK", + name: "crypto", }, }; export const userSignedPublicKey: KeyDefinitionLike = { key: "userSignedPublicKey", stateDefinition: { - name: "CRYPTO_DISK", + name: "crypto", }, }; export const accountSecurityState: KeyDefinitionLike = { key: "accountSecurityState", stateDefinition: { - name: "CRYPTO_DISK", + name: "crypto", + }, +}; + +export const accountCryptographicState: KeyDefinitionLike = { + key: "accountCryptographicState", + stateDefinition: { + name: "crypto", }, }; export class RemoveUserEncryptedPrivateKey extends Migrator<74, 75> { + async migrate(helper: MigrationHelper): Promise { const accounts = await helper.getAccounts(); - async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { - // Remove privateKey - const key = await helper.getFromUser(userId, userEncryptedPrivateKey); - if (key != null) { + for (const { userId } of accounts) { + // Check if account cryptographic state already exists + const existingAccountCryptoState = await helper.getFromUser(userId, accountCryptographicState); + + // Gather all individual cryptographic key state parts + const privateKey = await helper.getFromUser(userId, userEncryptedPrivateKey); + const signingKey = await helper.getFromUser(userId, userKeyEncryptedSigningKey); + const signedPubKey = await helper.getFromUser(userId, userSignedPublicKey); + const accountSecurity = await helper.getFromUser(userId, accountSecurityState); + + // Only migrate if account cryptographic state does not exist + if (!existingAccountCryptoState) { + // Build the new account cryptographic state object + let newAccountCryptographicState: WrappedAccountCryptographicState; + if (privateKey != null && signingKey == null && signedPubKey == null && accountSecurity == null) { + newAccountCryptographicState = { V1: { private_key: privateKey as EncString } }; + await helper.setToUser(userId, accountCryptographicState, newAccountCryptographicState); + } else if (privateKey != null && signingKey != null && signedPubKey != null && accountSecurity != null) { + newAccountCryptographicState = { + V2: { + private_key: privateKey as EncString, + signing_key: signingKey as EncString, + signed_public_key: signedPubKey as SignedPublicKey, + security_state: accountSecurity as SignedSecurityState, + }, + }; + await helper.setToUser(userId, accountCryptographicState, newAccountCryptographicState); + } else { + helper.logService.warning(`Incomplete cryptographic state for user ${userId}, skipping migration of account cryptographic state.`); + } + } + + // Always remove the old states + if (privateKey != null) { await helper.removeFromUser(userId, userEncryptedPrivateKey); } - // Remove userSigningKey - const signingKey = await helper.getFromUser(userId, userKeyEncryptedSigningKey); if (signingKey != null) { await helper.removeFromUser(userId, userKeyEncryptedSigningKey); } - // Remove userSignedPublicKey - const signedPubKey = await helper.getFromUser(userId, userSignedPublicKey); if (signedPubKey != null) { await helper.removeFromUser(userId, userSignedPublicKey); } - // Remove accountSecurityState - const accountSecurity = await helper.getFromUser(userId, accountSecurityState); if (accountSecurity != null) { await helper.removeFromUser(userId, accountSecurityState); } } - await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); } async rollback(helper: MigrationHelper): Promise {