1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[PM-10754] Store DeviceKey In Backup Storage Location (#10469)

* Implement Backup Storage Location For Browser Disk

* Remove Testing Change

* Remove Comment

* Add Comment For `disk-backup-local-storage`

* Require Matching `valuesRequireDeserialization` values
This commit is contained in:
Justin Baur
2024-08-12 13:29:22 -04:00
committed by GitHub
parent 334601e74f
commit a7adf952db
12 changed files with 164 additions and 8 deletions

View File

@@ -25,9 +25,12 @@ export type ClientLocations = {
/**
* Overriding storage location for browser clients.
*
* "memory-large-object" is used to store non-countable objects in memory. This exists due to limited persistent memory available to browser extensions.
* `"memory-large-object"` is used to store non-countable objects in memory. This exists due to limited persistent memory available to browser extensions.
*
* `"disk-backup-local-storage"` is used to store object in both disk and in `localStorage`. Data is stored in both locations but is only retrieved
* from `localStorage` when a null-ish value is retrieved from disk first.
*/
browser: StorageLocation | "memory-large-object";
browser: StorageLocation | "memory-large-object" | "disk-backup-local-storage";
/**
* Overriding storage location for desktop clients.
*/

View File

@@ -46,6 +46,7 @@ export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", {
web: "disk-local",
browser: "disk-backup-local-storage",
});
export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk");
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");

View File

@@ -0,0 +1,59 @@
import { AbstractStorageService, ObservableStorageService } from "../abstractions/storage.service";
import { StorageOptions } from "../models/domain/storage-options";
export class PrimarySecondaryStorageService
implements AbstractStorageService, ObservableStorageService
{
// Only follow the primary storage service as updates should all be done to both
updates$ = this.primaryStorageService.updates$;
constructor(
private readonly primaryStorageService: AbstractStorageService & ObservableStorageService,
// Secondary service doesn't need to be observable as the only `updates$` are listened to from the primary store
private readonly secondaryStorageService: AbstractStorageService,
) {
if (
primaryStorageService.valuesRequireDeserialization !==
secondaryStorageService.valuesRequireDeserialization
) {
throw new Error(
"Differing values for valuesRequireDeserialization between storage services is not supported.",
);
}
}
get valuesRequireDeserialization(): boolean {
return this.primaryStorageService.valuesRequireDeserialization;
}
async get<T>(key: string, options?: StorageOptions): Promise<T> {
const primaryValue = await this.primaryStorageService.get<T>(key, options);
// If it's null-ish try the secondary location for its value
if (primaryValue == null) {
return await this.secondaryStorageService.get<T>(key, options);
}
return primaryValue;
}
async has(key: string, options?: StorageOptions): Promise<boolean> {
return (
(await this.primaryStorageService.has(key, options)) ||
(await this.secondaryStorageService.has(key, options))
);
}
async save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
await Promise.allSettled([
this.primaryStorageService.save(key, obj, options),
this.secondaryStorageService.save(key, obj, options),
]);
}
async remove(key: string, options?: StorageOptions): Promise<void> {
await Promise.allSettled([
this.primaryStorageService.remove(key, options),
this.secondaryStorageService.remove(key, options),
]);
}
}

View File

@@ -0,0 +1,57 @@
import { Observable, Subject } from "rxjs";
import {
AbstractStorageService,
ObservableStorageService,
StorageUpdate,
} from "../abstractions/storage.service";
import { StorageOptions } from "../models/domain/storage-options";
export class WindowStorageService implements AbstractStorageService, ObservableStorageService {
private readonly updatesSubject = new Subject<StorageUpdate>();
updates$: Observable<StorageUpdate>;
constructor(private readonly storage: Storage) {
this.updates$ = this.updatesSubject.asObservable();
}
get valuesRequireDeserialization(): boolean {
return true;
}
get<T>(key: string, options?: StorageOptions): Promise<T> {
const jsonValue = this.storage.getItem(key);
if (jsonValue != null) {
return Promise.resolve(JSON.parse(jsonValue) as T);
}
return Promise.resolve(null);
}
async has(key: string, options?: StorageOptions): Promise<boolean> {
return (await this.get(key, options)) != null;
}
save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
if (obj == null) {
return this.remove(key, options);
}
if (obj instanceof Set) {
obj = Array.from(obj) as T;
}
this.storage.setItem(key, JSON.stringify(obj));
this.updatesSubject.next({ key, updateType: "save" });
}
remove(key: string, options?: StorageOptions): Promise<void> {
this.storage.removeItem(key);
this.updatesSubject.next({ key, updateType: "remove" });
return Promise.resolve();
}
getKeys(): string[] {
return Object.keys(this.storage);
}
}