1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

PM-5273 Encrypted and Decrypted ciphers migration to state provider

This commit is contained in:
Carlos Gonçalves
2024-03-06 09:07:59 +00:00
parent 60ac34182d
commit 9fb46cecf3
7 changed files with 126 additions and 74 deletions

View File

@@ -63,4 +63,4 @@ export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", {
web: "disk-local", web: "disk-local",
}); });
export const LOCAL_DATA = new StateDefinition("localData", "disk", { web: "disk-local" }); export const CIPHERS_DISK = new StateDefinition("localData", "disk", { web: "disk-local" });

View File

@@ -72,7 +72,7 @@ describe("LocalDataMigrator", () => {
let helper: MockProxy<MigrationHelper>; let helper: MockProxy<MigrationHelper>;
let sut: LocalDataMigrator; let sut: LocalDataMigrator;
const keyDefinitionLike = { const keyDefinitionLike = {
key: "local_data", key: "ciphers_disk",
stateDefinition: { stateDefinition: {
name: "localData", name: "localData",
}, },

View File

@@ -10,8 +10,8 @@ type LocalData = {
lastLaunched?: number; lastLaunched?: number;
}; };
const LOCAL_DATA: KeyDefinitionLike = { const CIPHERS_DISK: KeyDefinitionLike = {
key: "local_data", key: "ciphers_disk",
stateDefinition: { stateDefinition: {
name: "localData", name: "localData",
}, },
@@ -23,7 +23,7 @@ export class LocalDataMigrator extends Migrator<22, 23> {
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> { async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
const value = account?.localData; const value = account?.localData;
if (value != null) { if (value != null) {
await helper.setToUser(userId, LOCAL_DATA, value); await helper.setToUser(userId, CIPHERS_DISK, value);
delete account.LocalData; delete account.LocalData;
await helper.set(userId, account); await helper.set(userId, account);
} }
@@ -35,14 +35,14 @@ export class LocalDataMigrator extends Migrator<22, 23> {
async rollback(helper: MigrationHelper): Promise<void> { async rollback(helper: MigrationHelper): Promise<void> {
const accounts = await helper.getAccounts<ExpectedAccountType>(); const accounts = await helper.getAccounts<ExpectedAccountType>();
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> { async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
const value = await helper.getFromUser(userId, LOCAL_DATA); const value = await helper.getFromUser(userId, CIPHERS_DISK);
if (account) { if (account) {
account.localData = Object.assign(account.localData ?? {}, { account.localData = Object.assign(account.localData ?? {}, {
localData: value, localData: value,
}); });
await helper.set(userId, account); await helper.set(userId, account);
} }
await helper.setToUser(userId, LOCAL_DATA, null); await helper.setToUser(userId, CIPHERS_DISK, null);
} }
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);

View File

@@ -88,4 +88,5 @@ export abstract class CipherService {
asAdmin?: boolean, asAdmin?: boolean,
) => Promise<void>; ) => Promise<void>;
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>; getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>;
decryptCiphers: (ciphers: Cipher[]) => Promise<CipherView[]>;
} }

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type"; import { CipherType } from "../../enums/cipher-type";
import { CipherResponse } from "../response/cipher.response"; import { CipherResponse } from "../response/cipher.response";
@@ -84,4 +86,8 @@ export class CipherData {
this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph)); this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph));
} }
} }
static fromJSON(obj: Jsonify<CipherData>) {
return Object.assign(new CipherData(), obj);
}
} }

View File

@@ -20,7 +20,13 @@ import Domain from "../../platform/models/domain/domain-base";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { ActiveUserState, KeyDefinition, LOCAL_DATA, StateProvider } from "../../platform/state"; import {
ActiveUserState,
KeyDefinition,
CIPHERS_DISK,
StateProvider,
DerivedState,
} from "../../platform/state";
import { UserKey, OrgKey } from "../../types/key"; import { UserKey, OrgKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
@@ -54,9 +60,11 @@ import { CipherView } from "../models/view/cipher.view";
import { FieldView } from "../models/view/field.view"; import { FieldView } from "../models/view/field.view";
import { PasswordHistoryView } from "../models/view/password-history.view"; import { PasswordHistoryView } from "../models/view/password-history.view";
import { DECRYPTED_CIPHERS, ENCRYPTED_CIPHERS } from "./key-state/ciphers.state";
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0"); const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
const LOCAL_DATA_KEY = new KeyDefinition<Record<string, LocalData>>(LOCAL_DATA, "local_data", { const CIPHERS_DISK_KEY = new KeyDefinition<Record<string, LocalData>>(CIPHERS_DISK, "localData", {
deserializer: (obj) => obj, deserializer: (obj) => obj,
}); });
@@ -66,10 +74,12 @@ export class CipherService implements CipherServiceAbstraction {
); );
localData$: Observable<Record<string, LocalData>>; localData$: Observable<Record<string, LocalData>>;
ciphers$: Observable<Record<string, CipherData>>;
cipherViews$: Observable<CipherView[]>;
private localDataState: ActiveUserState<Record<string, LocalData>>; private localDataState: ActiveUserState<Record<string, LocalData>>;
private encryptedCiphersState: ActiveUserState<Record<string, CipherData>>;
private stateProviderFlag: boolean; private decryptedCiphersState: DerivedState<CipherView[]>;
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
@@ -84,21 +94,25 @@ export class CipherService implements CipherServiceAbstraction {
private configService: ConfigServiceAbstraction, private configService: ConfigServiceAbstraction,
private stateProvider: StateProvider, private stateProvider: StateProvider,
) { ) {
this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY); this.localDataState = this.stateProvider.getActive(CIPHERS_DISK_KEY);
this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS);
this.decryptedCiphersState = this.stateProvider.getDerived(
this.encryptedCiphersState.state$,
DECRYPTED_CIPHERS,
{ cipherService: this },
);
this.localData$ = this.localDataState.state$; this.localData$ = this.localDataState.state$;
this.ciphers$ = this.encryptedCiphersState.state$;
//TODO remove this before opening the PR and references to 5273 this.cipherViews$ = this.decryptedCiphersState.state$;
this.stateProviderFlag = false;
} }
async getDecryptedCipherCache(): Promise<CipherView[]> { async getDecryptedCipherCache(): Promise<CipherView[]> {
const decryptedCiphers = await this.stateService.getDecryptedCiphers(); const decryptedCiphers = await firstValueFrom(this.cipherViews$);
return decryptedCiphers; return decryptedCiphers;
} }
async setDecryptedCipherCache(value: CipherView[]) { async setDecryptedCipherCache(value: CipherView[]) {
await this.stateService.setDecryptedCiphers(value);
if (this.searchService != null) { if (this.searchService != null) {
if (value == null) { if (value == null) {
this.searchService.clearIndex(); this.searchService.clearIndex();
@@ -280,27 +294,20 @@ export class CipherService implements CipherServiceAbstraction {
} }
async get(id: string): Promise<Cipher> { async get(id: string): Promise<Cipher> {
const ciphers = await this.stateService.getEncryptedCiphers(); const ciphers = await firstValueFrom(this.ciphers$);
// eslint-disable-next-line // eslint-disable-next-line
if (ciphers == null || !ciphers.hasOwnProperty(id)) { if (ciphers == null || !ciphers.hasOwnProperty(id)) {
return null; return null;
} }
//5273 const localData = await firstValueFrom(this.localData$);
let localData;
if (this.stateProviderFlag) {
localData = await firstValueFrom(this.localData$);
} else {
localData = await this.stateService.getLocalData();
}
return new Cipher(ciphers[id], localData ? localData[id] : null); return new Cipher(ciphers[id], localData ? localData[id] : null);
} }
async getAll(): Promise<Cipher[]> { async getAll(): Promise<Cipher[]> {
//const localData = await firstValueFrom(this.localData$); const localData = await firstValueFrom(this.localData$);
const localData = await this.stateService.getLocalData(); const ciphers = await firstValueFrom(this.ciphers$);
const ciphers = await this.stateService.getEncryptedCiphers();
const response: Cipher[] = []; const response: Cipher[] = [];
for (const id in ciphers) { for (const id in ciphers) {
// eslint-disable-next-line // eslint-disable-next-line
@@ -318,7 +325,13 @@ export class CipherService implements CipherServiceAbstraction {
return await this.getDecryptedCipherCache(); return await this.getDecryptedCipherCache();
} }
const ciphers = await this.getAll(); const decCiphers = await this.decryptCiphers(await this.getAll());
await this.setDecryptedCipherCache(decCiphers);
return decCiphers;
}
async decryptCiphers(ciphers: Cipher[]) {
const orgKeys = await this.cryptoService.getOrgKeys(); const orgKeys = await this.cryptoService.getOrgKeys();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport(); const userKey = await this.cryptoService.getUserKeyWithLegacySupport();
if (Object.keys(orgKeys).length === 0 && userKey == null) { if (Object.keys(orgKeys).length === 0 && userKey == null) {
@@ -346,7 +359,6 @@ export class CipherService implements CipherServiceAbstraction {
.flat() .flat()
.sort(this.getLocaleSortingFunction()); .sort(this.getLocaleSortingFunction());
await this.setDecryptedCipherCache(decCiphers);
return decCiphers; return decCiphers;
} }
@@ -465,13 +477,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
async updateLastUsedDate(id: string): Promise<void> { async updateLastUsedDate(id: string): Promise<void> {
//5273 let ciphersLocalData = await firstValueFrom(this.localData$);
let ciphersLocalData: { [cipherId: string]: LocalData } | Record<string, LocalData>;
if (this.stateProviderFlag) {
ciphersLocalData = await firstValueFrom(this.localData$);
} else {
ciphersLocalData = await this.stateService.getLocalData();
}
if (!ciphersLocalData) { if (!ciphersLocalData) {
ciphersLocalData = {}; ciphersLocalData = {};
@@ -485,14 +491,9 @@ export class CipherService implements CipherServiceAbstraction {
}; };
} }
//5273 await this.localDataState.update(() => ciphersLocalData);
if (this.stateProviderFlag) {
await this.localDataState.update(() => ciphersLocalData);
} else {
await this.stateService.setLocalData(ciphersLocalData);
}
const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); const decryptedCipherCache = await firstValueFrom(this.cipherViews$);
if (!decryptedCipherCache) { if (!decryptedCipherCache) {
return; return;
} }
@@ -504,17 +505,12 @@ export class CipherService implements CipherServiceAbstraction {
break; break;
} }
} }
await this.stateService.setDecryptedCiphers(decryptedCipherCache); //TODO Is this the right action? Or should the force be used only for clearing
await this.decryptedCiphersState.forceValue(decryptedCipherCache);
} }
async updateLastLaunchedDate(id: string): Promise<void> { async updateLastLaunchedDate(id: string): Promise<void> {
//5273 let ciphersLocalData = await firstValueFrom(this.localData$);
let ciphersLocalData: { [cipherId: string]: LocalData } | Record<string, LocalData>;
if (this.stateProviderFlag) {
ciphersLocalData = await firstValueFrom(this.localData$);
} else {
ciphersLocalData = await this.stateService.getLocalData();
}
if (!ciphersLocalData) { if (!ciphersLocalData) {
ciphersLocalData = {}; ciphersLocalData = {};
@@ -528,14 +524,9 @@ export class CipherService implements CipherServiceAbstraction {
}; };
} }
//5273 await this.localDataState.update(() => ciphersLocalData);
if (this.stateProviderFlag) {
await this.localDataState.update(() => ciphersLocalData);
} else {
await this.stateService.setLocalData(ciphersLocalData);
}
const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); const decryptedCipherCache = await firstValueFrom(this.cipherViews$);
if (!decryptedCipherCache) { if (!decryptedCipherCache) {
return; return;
} }
@@ -547,7 +538,8 @@ export class CipherService implements CipherServiceAbstraction {
break; break;
} }
} }
await this.stateService.setDecryptedCiphers(decryptedCipherCache); //TODO Is this the right action? Or should the force be used only for clearing
await this.decryptedCiphersState.forceValue(decryptedCipherCache);
} }
async saveNeverDomain(domain: string): Promise<void> { async saveNeverDomain(domain: string): Promise<void> {
@@ -730,7 +722,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
async upsert(cipher: CipherData | CipherData[]): Promise<any> { async upsert(cipher: CipherData | CipherData[]): Promise<any> {
let ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
ciphers = {}; ciphers = {};
} }
@@ -749,7 +741,12 @@ export class CipherService implements CipherServiceAbstraction {
async replace(ciphers: { [id: string]: CipherData }): Promise<any> { async replace(ciphers: { [id: string]: CipherData }): Promise<any> {
await this.clearDecryptedCiphersState(); await this.clearDecryptedCiphersState();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
} }
async clear(userId?: string): Promise<any> { async clear(userId?: string): Promise<any> {
@@ -760,7 +757,7 @@ export class CipherService implements CipherServiceAbstraction {
async moveManyWithServer(ids: string[], folderId: string): Promise<any> { async moveManyWithServer(ids: string[], folderId: string): Promise<any> {
await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId));
let ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
ciphers = {}; ciphers = {};
} }
@@ -773,11 +770,16 @@ export class CipherService implements CipherServiceAbstraction {
}); });
await this.clearCache(); await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
} }
async delete(id: string | string[]): Promise<any> { async delete(id: string | string[]): Promise<any> {
const ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
return; return;
} }
@@ -794,7 +796,12 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
} }
async deleteWithServer(id: string, asAdmin = false): Promise<any> { async deleteWithServer(id: string, asAdmin = false): Promise<any> {
@@ -818,7 +825,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
async deleteAttachment(id: string, attachmentId: string): Promise<void> { async deleteAttachment(id: string, attachmentId: string): Promise<void> {
const ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
// eslint-disable-next-line // eslint-disable-next-line
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) { if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) {
@@ -832,7 +839,12 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
} }
async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<void> { async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<void> {
@@ -915,7 +927,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
async softDelete(id: string | string[]): Promise<any> { async softDelete(id: string | string[]): Promise<any> {
const ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
return; return;
} }
@@ -934,7 +946,12 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
} }
async softDeleteWithServer(id: string, asAdmin = false): Promise<any> { async softDeleteWithServer(id: string, asAdmin = false): Promise<any> {
@@ -961,7 +978,7 @@ export class CipherService implements CipherServiceAbstraction {
async restore( async restore(
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) { ) {
const ciphers = await this.stateService.getEncryptedCiphers(); let ciphers = await firstValueFrom(this.ciphers$);
if (ciphers == null) { if (ciphers == null) {
return; return;
} }
@@ -981,7 +998,12 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.stateService.setEncryptedCiphers(ciphers); await this.encryptedCiphersState.update(() => {
if (ciphers == null) {
ciphers = {};
}
return ciphers;
});
} }
async restoreWithServer(id: string, asAdmin = false): Promise<any> { async restoreWithServer(id: string, asAdmin = false): Promise<any> {
@@ -1348,11 +1370,11 @@ export class CipherService implements CipherServiceAbstraction {
} }
private async clearEncryptedCiphersState(userId?: string) { private async clearEncryptedCiphersState(userId?: string) {
await this.stateService.setEncryptedCiphers(null, { userId: userId }); await this.encryptedCiphersState.update(() => ({}));
} }
private async clearDecryptedCiphersState(userId?: string) { private async clearDecryptedCiphersState(userId?: string) {
await this.stateService.setDecryptedCiphers(null, { userId: userId }); await this.decryptedCiphersState.forceValue([]);
this.clearSortedCiphers(); this.clearSortedCiphers();
} }

View File

@@ -0,0 +1,23 @@
import { Jsonify } from "type-fest";
import { CIPHERS_DISK, DeriveDefinition, KeyDefinition } from "../../../platform/state";
import { CipherService } from "../../abstractions/cipher.service";
import { CipherData } from "../../models/data/cipher.data";
import { Cipher } from "../../models/domain/cipher";
import { CipherView } from "../../models/view/cipher.view";
export const ENCRYPTED_CIPHERS = KeyDefinition.record<CipherData>(CIPHERS_DISK, "ciphers", {
deserializer: (obj: Jsonify<CipherData>) => CipherData.fromJSON(obj),
});
export const DECRYPTED_CIPHERS = DeriveDefinition.from<
Record<string, CipherData>,
CipherView[],
{ cipherService: CipherService }
>(ENCRYPTED_CIPHERS, {
deserializer: (obj) => obj.map((c) => CipherView.fromJSON(c)),
derive: async (from, { cipherService }) => {
const ciphers = Object.values(from || {}).map((c) => new Cipher(c));
return await cipherService.decryptCiphers(ciphers);
},
});