1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 22:44:11 +00:00

Add migration

This commit is contained in:
Bernd Schoolmann
2026-01-07 13:38:17 +01:00
parent 6b672f8cc3
commit 34058b4486
2 changed files with 173 additions and 24 deletions

View File

@@ -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,
});
});
});

View File

@@ -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<unknown>;
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<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
// 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<void> {