mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
PS-813 Add memory storage to state service (#2892)
* Use abstract methods and generics in StorageService * Prepend `Abstract` to abstract classes * Create session browser storage service * Use memory storage service for state memory * Inject memory storage service * Maintain filename extensions to help ide formatting * Preserve state if it's still in memory * Use jslib's memory storage service * linter * Create prototypes on stored objects * standardize package scripts * Add type safety to `withPrototype` decorators * webpack notify manifest version * Fix desktop * linter * Fix script * Improve prototye application * do not change prototype if it already matches desired * fix error with object values prototype application * Handle null state * Apply prototypes to browser-specific state * Add angular language server to recommended extensions * Improve browser state service tests * Start testing state Service * Fix abstract returns * Move test setup files to not be picked up by default glob matchers * Add key generation service * Add low-dependency encrypt service * Back crypto service with encrypt service. We'll want to work items that don't require state over to encrypt service * Add new storage service and tests * Properly init more stored values * Fix reload issues when state service is recovering state from session storage Co-authored-by: Thomas Avery <Thomas-Avery@users.noreply.github.com> Co-authored-by: Justin Baur <admin@justinbaur.com> * Simplify encrypt service * Do not log mac failures for local-backed session storage * `content` changed to `main` in #2245 * Fix CLI * Remove loggin * PR feedback * Merge remote-tracking branch 'origin/master' into add-memory-storage-to-state-service * Fix desktop * Fix decrypt method signature * Minify if not development * Key is required Co-authored-by: Thomas Avery <Thomas-Avery@users.noreply.github.com> Co-authored-by: Justin Baur <admin@justinbaur.com>
This commit is contained in:
7
libs/common/src/abstractions/abstractEncrypt.service.ts
Normal file
7
libs/common/src/abstractions/abstractEncrypt.service.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
|
||||
export abstract class AbstractEncryptService {
|
||||
abstract encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
abstract decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise<string>;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { StorageOptions } from "../models/domain/storageOptions";
|
||||
|
||||
export abstract class StorageService {
|
||||
get: <T>(key: string, options?: StorageOptions) => Promise<T>;
|
||||
has: (key: string, options?: StorageOptions) => Promise<boolean>;
|
||||
save: (key: string, obj: any, options?: StorageOptions) => Promise<any>;
|
||||
remove: (key: string, options?: StorageOptions) => Promise<any>;
|
||||
export abstract class AbstractStorageService {
|
||||
abstract get<T>(key: string, options?: StorageOptions): Promise<T>;
|
||||
abstract has(key: string, options?: StorageOptions): Promise<boolean>;
|
||||
abstract save<T>(key: string, obj: T, options?: StorageOptions): Promise<void>;
|
||||
abstract remove(key: string, options?: StorageOptions): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: TEncrypted;
|
||||
decrypted?: TDecrypted;
|
||||
decryptedSerialized?: string;
|
||||
}
|
||||
|
||||
export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
||||
@@ -76,6 +77,7 @@ export class AccountKeys {
|
||||
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
||||
legacyEtmKey?: SymmetricCryptoKey;
|
||||
publicKey?: ArrayBuffer;
|
||||
publicKeySerialized?: string;
|
||||
apiKeyClientSecret?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,4 +54,22 @@ export class SymmetricCryptoKey {
|
||||
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
|
||||
}
|
||||
}
|
||||
|
||||
static initFromJson(jsonResult: SymmetricCryptoKey): SymmetricCryptoKey {
|
||||
if (jsonResult == null) {
|
||||
return jsonResult;
|
||||
}
|
||||
|
||||
if (jsonResult.keyB64 != null) {
|
||||
jsonResult.key = Utils.fromB64ToArray(jsonResult.keyB64).buffer;
|
||||
}
|
||||
if (jsonResult.encKeyB64 != null) {
|
||||
jsonResult.encKey = Utils.fromB64ToArray(jsonResult.encKeyB64).buffer;
|
||||
}
|
||||
if (jsonResult.macKeyB64 != null) {
|
||||
jsonResult.macKey = Utils.fromB64ToArray(jsonResult.macKeyB64).buffer;
|
||||
}
|
||||
|
||||
return jsonResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/appId.service";
|
||||
import { StorageService } from "../abstractions/storage.service";
|
||||
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { HtmlStorageLocation } from "../enums/htmlStorageLocation";
|
||||
import { Utils } from "../misc/utils";
|
||||
|
||||
export class AppIdService implements AppIdServiceAbstraction {
|
||||
constructor(private storageService: StorageService) {}
|
||||
constructor(private storageService: AbstractStorageService) {}
|
||||
|
||||
getAppId(): Promise<string> {
|
||||
return this.makeAndGetAppId("appId");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as bigInt from "big-integer";
|
||||
|
||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
@@ -23,6 +24,7 @@ import { ProfileProviderResponse } from "../models/response/profileProviderRespo
|
||||
export class CryptoService implements CryptoServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private encryptService: AbstractEncryptService,
|
||||
protected platformUtilService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
protected stateService: StateService
|
||||
@@ -503,23 +505,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return this.buildEncKey(key, encKey.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated June 22 2022: This method has been moved to encryptService.
|
||||
* All callers should use this service to grab the relevant key and use encryptService for encryption instead.
|
||||
* This method will be removed once all existing code has been refactored to use encryptService.
|
||||
*/
|
||||
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (plainValue == null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
key = await this.getKeyForEncryption(key);
|
||||
|
||||
let plainBuf: ArrayBuffer;
|
||||
if (typeof plainValue === "string") {
|
||||
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
|
||||
} else {
|
||||
plainBuf = plainValue;
|
||||
}
|
||||
|
||||
const encObj = await this.aesEncrypt(plainBuf, key);
|
||||
const iv = Utils.fromBufferToB64(encObj.iv);
|
||||
const data = Utils.fromBufferToB64(encObj.data);
|
||||
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
|
||||
return new EncString(encObj.key.encType, data, iv, mac);
|
||||
return await this.encryptService.encrypt(plainValue, key);
|
||||
}
|
||||
|
||||
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
@@ -618,13 +612,9 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string> {
|
||||
return await this.aesDecryptToUtf8(
|
||||
encString.encryptionType,
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key
|
||||
);
|
||||
key = await this.getKeyForEncryption(key);
|
||||
key = await this.resolveLegacyKey(encString.encryptionType, key);
|
||||
return await this.encryptService.decryptToUtf8(encString, key);
|
||||
}
|
||||
|
||||
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||
@@ -754,6 +744,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
: await this.stateService.getCryptoMasterKeyBiometric({ userId: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated June 22 2022: This method has been moved to encryptService.
|
||||
* All callers should use encryptService instead. This method will be removed once all existing code has been refactored to use encryptService.
|
||||
*/
|
||||
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = await this.getKeyForEncryption(key);
|
||||
@@ -770,43 +764,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private async aesDecryptToUtf8(
|
||||
encType: EncryptionType,
|
||||
data: string,
|
||||
iv: string,
|
||||
mac: string,
|
||||
key: SymmetricCryptoKey
|
||||
): Promise<string> {
|
||||
const keyForEnc = await this.getKeyForEncryption(key);
|
||||
const theKey = await this.resolveLegacyKey(encType, keyForEnc);
|
||||
|
||||
if (theKey.macKey != null && mac == null) {
|
||||
this.logService.error("mac required.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (theKey.encType !== encType) {
|
||||
this.logService.error("encType unavailable.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(data, iv, mac, theKey);
|
||||
if (fastParams.macKey != null && fastParams.mac != null) {
|
||||
const computedMac = await this.cryptoFunctionService.hmacFast(
|
||||
fastParams.macData,
|
||||
fastParams.macKey,
|
||||
"sha256"
|
||||
);
|
||||
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
||||
if (!macsEqual) {
|
||||
this.logService.error("mac failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.aesDecryptFast(fastParams);
|
||||
}
|
||||
|
||||
private async aesDecryptToBytes(
|
||||
encType: EncryptionType,
|
||||
data: ArrayBuffer,
|
||||
|
||||
94
libs/common/src/services/encrypt.service.ts
Normal file
94
libs/common/src/services/encrypt.service.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||
import { EncryptedObject } from "@bitwarden/common/models/domain/encryptedObject";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
|
||||
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||
|
||||
export class EncryptService implements AbstractEncryptService {
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private logService: LogService,
|
||||
private logMacFailures: boolean
|
||||
) {}
|
||||
|
||||
async encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (key == null) {
|
||||
throw new Error("no encryption key provided.");
|
||||
}
|
||||
|
||||
if (plainValue == null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
let plainBuf: ArrayBuffer;
|
||||
if (typeof plainValue === "string") {
|
||||
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
|
||||
} else {
|
||||
plainBuf = plainValue;
|
||||
}
|
||||
|
||||
const encObj = await this.aesEncrypt(plainBuf, key);
|
||||
const iv = Utils.fromBufferToB64(encObj.iv);
|
||||
const data = Utils.fromBufferToB64(encObj.data);
|
||||
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
|
||||
return new EncString(encObj.key.encType, data, iv, mac);
|
||||
}
|
||||
|
||||
async decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
|
||||
if (key?.macKey != null && encString?.mac == null) {
|
||||
this.logService.error("mac required.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.encType !== encString.encryptionType) {
|
||||
this.logService.error("encType unavailable.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key
|
||||
);
|
||||
if (fastParams.macKey != null && fastParams.mac != null) {
|
||||
const computedMac = await this.cryptoFunctionService.hmacFast(
|
||||
fastParams.macData,
|
||||
fastParams.macKey,
|
||||
"sha256"
|
||||
);
|
||||
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
||||
if (!macsEqual) {
|
||||
this.logMacFailed("mac failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.cryptoFunctionService.aesDecryptFast(fastParams);
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = key;
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
|
||||
|
||||
if (obj.key.macKey != null) {
|
||||
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
|
||||
macData.set(new Uint8Array(obj.iv), 0);
|
||||
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
|
||||
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private logMacFailed(msg: string) {
|
||||
if (this.logMacFailures) {
|
||||
this.logService.error(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
libs/common/src/services/memoryStorage.service.ts
Normal file
30
libs/common/src/services/memoryStorage.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
|
||||
|
||||
export class MemoryStorageService implements AbstractStorageService {
|
||||
private store = new Map<string, any>();
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
if (this.store.has(key)) {
|
||||
const obj = this.store.get(key);
|
||||
return Promise.resolve(obj as T);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
return this.get(key) != null;
|
||||
}
|
||||
|
||||
save(key: string, obj: any): Promise<any> {
|
||||
if (obj == null) {
|
||||
return this.remove(key);
|
||||
}
|
||||
this.store.set(key, obj);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
this.store.delete(key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { StorageService } from "../abstractions/storage.service";
|
||||
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||
import { HtmlStorageLocation } from "../enums/htmlStorageLocation";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { StateVersion } from "../enums/stateVersion";
|
||||
@@ -132,8 +132,8 @@ export class StateMigrationService<
|
||||
TAccount extends Account = Account
|
||||
> {
|
||||
constructor(
|
||||
protected storageService: StorageService,
|
||||
protected secureStorageService: StorageService,
|
||||
protected storageService: AbstractStorageService,
|
||||
protected secureStorageService: AbstractStorageService,
|
||||
protected stateFactory: StateFactory<TGlobalState, TAccount>
|
||||
) {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user