1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-5273] Migrate state in CipherService (#8314)

* PM-5273 Initial migration work for localData

* PM-5273 Encrypted and Decrypted ciphers migration to state provider

* pm-5273 Update references

* pm5273 Ensure prototype on cipher

* PM-5273 Add CipherId

* PM-5273 Remove migrated methods and updated references

* pm-5273 Fix versions

* PM-5273 Added missing options

* Conflict resolution

* Revert "Conflict resolution"

This reverts commit 0c0c2039ed.

* PM-5273 Fix PR comments

* Pm-5273 Fix comments

* PM-5273 Changed decryptedCiphers to use ActiveUserState

* PM-5273 Fix tests

* PM-5273 Fix pr comments
This commit is contained in:
Carlos Gonçalves
2024-04-16 17:37:03 +01:00
committed by GitHub
parent 62ed7e5abc
commit 06acdefa91
29 changed files with 525 additions and 305 deletions

View File

@@ -53,6 +53,7 @@ import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-m
import { SendMigrator } from "./migrations/54-move-encrypted-sends";
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
@@ -60,8 +61,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3;
export const CURRENT_VERSION = 56;
export const CURRENT_VERSION = 57;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -119,7 +119,8 @@ export function createMigrationBuilder() {
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53)
.with(SendMigrator, 53, 54)
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
.with(AuthRequestMigrator, 55, CURRENT_VERSION);
.with(AuthRequestMigrator, 55, 56)
.with(CipherServiceMigrator, 56, CURRENT_VERSION);
}
export async function currentVersion(

View File

@@ -0,0 +1,170 @@
import { MockProxy, any } from "jest-mock-extended";
import { MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import {
CIPHERS_DISK,
CIPHERS_DISK_LOCAL,
CipherServiceMigrator,
} from "./57-move-cipher-service-to-state-provider";
function exampleJSON() {
return {
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user1", "user2"],
user1: {
data: {
localData: {
"6865ba55-7966-4d63-b743-b12000d49631": {
lastUsedDate: 1708950970632,
},
"f895f099-6739-4cca-9d61-b12200d04bfa": {
lastUsedDate: 1709031916943,
},
},
ciphers: {
"cipher-id-10": {
id: "cipher-id-10",
},
"cipher-id-11": {
id: "cipher-id-11",
},
},
},
},
user2: {
data: {
otherStuff: "otherStuff5",
},
},
};
}
function rollbackJSON() {
return {
user_user1_ciphersLocal_localData: {
"6865ba55-7966-4d63-b743-b12000d49631": {
lastUsedDate: 1708950970632,
},
"f895f099-6739-4cca-9d61-b12200d04bfa": {
lastUsedDate: 1709031916943,
},
},
user_user1_ciphers_ciphers: {
"cipher-id-10": {
id: "cipher-id-10",
},
"cipher-id-11": {
id: "cipher-id-11",
},
},
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user1", "user2"],
user1: {
data: {},
},
user2: {
data: {
localData: {
otherStuff: "otherStuff3",
},
ciphers: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
},
};
}
describe("CipherServiceMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: CipherServiceMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 56);
sut = new CipherServiceMigrator(56, 57);
});
it("should remove local data and ciphers from all accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledWith("user1", {
data: {},
});
});
it("should migrate localData and ciphers to state provider for accounts that have the data", async () => {
await sut.migrate(helper);
expect(helper.setToUser).toHaveBeenCalledWith("user1", CIPHERS_DISK_LOCAL, {
"6865ba55-7966-4d63-b743-b12000d49631": {
lastUsedDate: 1708950970632,
},
"f895f099-6739-4cca-9d61-b12200d04bfa": {
lastUsedDate: 1709031916943,
},
});
expect(helper.setToUser).toHaveBeenCalledWith("user1", CIPHERS_DISK, {
"cipher-id-10": {
id: "cipher-id-10",
},
"cipher-id-11": {
id: "cipher-id-11",
},
});
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", CIPHERS_DISK_LOCAL, any());
expect(helper.setToUser).not.toHaveBeenCalledWith("user2", CIPHERS_DISK, any());
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 57);
sut = new CipherServiceMigrator(56, 57);
});
it.each(["user1", "user2"])("should null out new values", async (userId) => {
await sut.rollback(helper);
expect(helper.setToUser).toHaveBeenCalledWith(userId, CIPHERS_DISK_LOCAL, null);
expect(helper.setToUser).toHaveBeenCalledWith(userId, CIPHERS_DISK, null);
});
it("should add back localData and ciphers to all accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledWith("user1", {
data: {
localData: {
"6865ba55-7966-4d63-b743-b12000d49631": {
lastUsedDate: 1708950970632,
},
"f895f099-6739-4cca-9d61-b12200d04bfa": {
lastUsedDate: 1709031916943,
},
},
ciphers: {
"cipher-id-10": {
id: "cipher-id-10",
},
"cipher-id-11": {
id: "cipher-id-11",
},
},
},
});
});
it("should not add data back if data wasn't migrated or acct doesn't exist", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("user2", any());
});
});
});

View File

@@ -0,0 +1,79 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
type ExpectedAccountType = {
data: {
localData?: unknown;
ciphers?: unknown;
};
};
export const CIPHERS_DISK_LOCAL: KeyDefinitionLike = {
key: "localData",
stateDefinition: {
name: "ciphersLocal",
},
};
export const CIPHERS_DISK: KeyDefinitionLike = {
key: "ciphers",
stateDefinition: {
name: "ciphers",
},
};
export class CipherServiceMigrator extends Migrator<56, 57> {
async migrate(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
let updatedAccount = false;
//Migrate localData
const localData = account?.data?.localData;
if (localData != null) {
await helper.setToUser(userId, CIPHERS_DISK_LOCAL, localData);
delete account.data.localData;
updatedAccount = true;
}
//Migrate ciphers
const ciphers = account?.data?.ciphers;
if (ciphers != null) {
await helper.setToUser(userId, CIPHERS_DISK, ciphers);
delete account.data.ciphers;
updatedAccount = true;
}
if (updatedAccount) {
await helper.set(userId, account);
}
}
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
}
async rollback(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>();
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
//rollback localData
const localData = await helper.getFromUser(userId, CIPHERS_DISK_LOCAL);
if (account.data && localData != null) {
account.data.localData = localData;
await helper.set(userId, account);
}
await helper.setToUser(userId, CIPHERS_DISK_LOCAL, null);
//rollback ciphers
const ciphers = await helper.getFromUser(userId, CIPHERS_DISK);
if (account.data && ciphers != null) {
account.data.ciphers = ciphers;
await helper.set(userId, account);
}
await helper.setToUser(userId, CIPHERS_DISK, null);
}
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
}
}