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:
Matt Gibson 2022-06-27 13:38:12 -04:00 committed by GitHub
parent c044c5c770
commit 399b8c2b34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1575 additions and 370 deletions

View File

@ -4,7 +4,7 @@
"types": ["node"],
"allowSyntheticDefaultImports": true
},
"exclude": ["../src/test.ts", "../src/**/*.spec.ts", "../projects/**/*.spec.ts"],
"exclude": ["../src/test.setup.ts", "../src/**/*.spec.ts", "../projects/**/*.spec.ts"],
"include": ["../src/**/*", "../projects/**/*"],
"files": ["./typings.d.ts"]
}

View File

@ -6,6 +6,7 @@ module.exports = {
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
preset: "jest-preset-angular",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
prefix: "<rootDir>/",

View File

@ -3,12 +3,14 @@
"version": "0.0.0",
"scripts": {
"build": "webpack",
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack",
"build:watch": "webpack --watch",
"build:watch:MV3": "cross-env MANIFEST_VERSION=3 webpack --watch",
"build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch",
"build:prod": "cross-env NODE_ENV=production webpack",
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
"dist": "npm run build:prod && gulp dist",
"dist:chromeMV3": "cross-env MANIFEST_VERSION=3 npm run build:prod && gulp dist:chrome",
"dist:chrome": "npm run build:prod && gulp dist:chrome",
"dist:chrome:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && gulp dist:chrome",
"dist:firefox": "npm run build:prod && gulp dist:firefox",
"dist:opera": "npm run build:prod && gulp dist:opera",
"dist:safari": "npm run build:prod && gulp dist:safari",

View File

@ -24,7 +24,7 @@ import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstractions/send.service";
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { StorageService as StorageServiceAbstraction } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync.service";
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service";
@ -47,12 +47,14 @@ import { CipherService } from "@bitwarden/common/services/cipher.service";
import { CollectionService } from "@bitwarden/common/services/collection.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { ContainerService } from "@bitwarden/common/services/container.service";
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/services/environment.service";
import { EventService } from "@bitwarden/common/services/event.service";
import { ExportService } from "@bitwarden/common/services/export.service";
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
import { FolderService } from "@bitwarden/common/services/folder.service";
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { OrganizationService } from "@bitwarden/common/services/organization.service";
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
@ -79,11 +81,13 @@ import { AutofillService as AutofillServiceAbstraction } from "../services/abstr
import { StateService as StateServiceAbstraction } from "../services/abstractions/state.service";
import AutofillService from "../services/autofill.service";
import { BrowserCryptoService } from "../services/browserCrypto.service";
import BrowserLocalStorageService from "../services/browserLocalStorage.service";
import BrowserMessagingService from "../services/browserMessaging.service";
import BrowserMessagingPrivateModeBackgroundService from "../services/browserMessagingPrivateModeBackground.service";
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import BrowserStorageService from "../services/browserStorage.service";
import I18nService from "../services/i18n.service";
import { KeyGenerationService } from "../services/keyGeneration.service";
import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service";
import { StateService } from "../services/state.service";
import { VaultFilterService } from "../services/vaultFilter.service";
import VaultTimeoutService from "../services/vaultTimeout.service";
@ -100,8 +104,9 @@ import WebRequestBackground from "./webRequest.background";
export default class MainBackground {
messagingService: MessagingServiceAbstraction;
storageService: StorageServiceAbstraction;
secureStorageService: StorageServiceAbstraction;
storageService: AbstractStorageService;
secureStorageService: AbstractStorageService;
memoryStorageService: AbstractStorageService;
i18nService: I18nServiceAbstraction;
platformUtilsService: PlatformUtilsServiceAbstraction;
logService: LogServiceAbstraction;
@ -141,6 +146,7 @@ export default class MainBackground {
twoFactorService: TwoFactorServiceAbstraction;
vaultFilterService: VaultFilterService;
usernameGenerationService: UsernameGenerationServiceAbstraction;
encryptService: EncryptService;
onUpdatedRan: boolean;
onReplacedRan: boolean;
@ -181,9 +187,17 @@ export default class MainBackground {
this.messagingService = isPrivateMode
? new BrowserMessagingPrivateModeBackgroundService()
: new BrowserMessagingService();
this.storageService = new BrowserStorageService();
this.secureStorageService = new BrowserStorageService();
this.logService = new ConsoleLogService(false);
this.cryptoFunctionService = new WebCryptoFunctionService(window);
this.storageService = new BrowserLocalStorageService();
this.secureStorageService = new BrowserLocalStorageService();
this.memoryStorageService =
chrome.runtime.getManifest().manifest_version == 3
? new LocalBackedSessionStorageService(
new EncryptService(this.cryptoFunctionService, this.logService, false),
new KeyGenerationService(this.cryptoFunctionService)
)
: new MemoryStorageService();
this.stateMigrationService = new StateMigrationService(
this.storageService,
this.secureStorageService,
@ -192,6 +206,7 @@ export default class MainBackground {
this.stateService = new StateService(
this.storageService,
this.secureStorageService,
this.memoryStorageService,
this.logService,
this.stateMigrationService,
new StateFactory(GlobalState, Account)
@ -219,9 +234,10 @@ export default class MainBackground {
}
);
this.i18nService = new I18nService(BrowserApi.getUILanguage(window));
this.cryptoFunctionService = new WebCryptoFunctionService(window);
this.encryptService = new EncryptService(this.cryptoFunctionService, this.logService, true);
this.cryptoService = new BrowserCryptoService(
this.cryptoFunctionService,
this.encryptService,
this.platformUtilsService,
this.logService,
this.stateService

View File

@ -64,9 +64,7 @@
"unlimitedStorage",
"clipboardRead",
"clipboardWrite",
"idle",
"webRequest",
"declarativeNetRequest"
"idle"
],
"optional_permissions": ["nativeMessaging"],
"host_permissions": ["http://*/*", "https://*/*"],

View File

@ -30,12 +30,12 @@ export class PopupUtilsService {
return this.privateMode;
}
getContentScrollY(win: Window, scrollingContainer = "content"): number {
getContentScrollY(win: Window, scrollingContainer = "main"): number {
const content = win.document.getElementsByTagName(scrollingContainer)[0];
return content.scrollTop;
}
setContentScrollY(win: Window, scrollY: number, scrollingContainer = "content"): void {
setContentScrollY(win: Window, scrollY: number, scrollingContainer = "main"): void {
if (scrollY != null) {
const content = win.document.getElementsByTagName(scrollingContainer)[0];
content.scrollTop = scrollY;

View File

@ -4,6 +4,7 @@ import { LockGuard as BaseLockGuardService } from "@bitwarden/angular/guards/loc
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/guards/unauth.guard";
import {
JslibServicesModule,
MEMORY_STORAGE,
SECURE_STORAGE,
} from "@bitwarden/angular/services/jslib-services.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@ -34,7 +35,7 @@ import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abs
import { SendService } from "@bitwarden/common/abstractions/send.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StorageService as StorageServiceAbstraction } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SyncService } from "@bitwarden/common/abstractions/sync.service";
import { TokenService } from "@bitwarden/common/abstractions/token.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
@ -185,8 +186,8 @@ function getBgService<T>(service: keyof MainBackground) {
deps: [],
},
{
provide: StorageServiceAbstraction,
useFactory: getBgService<StorageServiceAbstraction>("storageService"),
provide: AbstractStorageService,
useFactory: getBgService<AbstractStorageService>("storageService"),
deps: [],
},
{ provide: AppIdService, useFactory: getBgService<AppIdService>("appIdService"), deps: [] },
@ -249,9 +250,13 @@ function getBgService<T>(service: keyof MainBackground) {
},
{
provide: SECURE_STORAGE,
useFactory: getBgService<StorageServiceAbstraction>("secureStorageService"),
useFactory: getBgService<AbstractStorageService>("secureStorageService"),
deps: [],
},
{
provide: MEMORY_STORAGE,
useFactory: getBgService<AbstractStorageService>("memoryStorageService"),
},
{
provide: StateServiceAbstraction,
useFactory: getBgService<StateServiceAbstraction>("stateService"),

View File

@ -1,11 +1,7 @@
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
export default class BrowserStorageService implements StorageService {
private chromeStorageApi: any;
constructor() {
this.chromeStorageApi = chrome.storage.local;
}
export default abstract class AbstractChromeStorageService implements AbstractStorageService {
protected abstract chromeStorageApi: any;
async get<T>(key: string): Promise<T> {
return new Promise((resolve) => {
@ -23,7 +19,7 @@ export default class BrowserStorageService implements StorageService {
return (await this.get(key)) != null;
}
async save(key: string, obj: any): Promise<any> {
async save(key: string, obj: any): Promise<void> {
if (obj == null) {
// Fix safari not liking null in set
return new Promise<void>((resolve) => {
@ -45,7 +41,7 @@ export default class BrowserStorageService implements StorageService {
});
}
async remove(key: string): Promise<any> {
async remove(key: string): Promise<void> {
return new Promise<void>((resolve) => {
this.chromeStorageApi.remove(key, () => {
resolve();

View File

@ -0,0 +1,5 @@
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
export interface AbstractKeyGenerationService {
makeEphemeralKey(numBytes?: number): Promise<SymmetricCryptoKey>;
}

View File

@ -0,0 +1,5 @@
import AbstractChromeStorageService from "./abstractChromeStorageApi.service";
export default class BrowserLocalStorageService extends AbstractChromeStorageService {
protected chromeStorageApi: any = chrome.storage.local;
}

View File

@ -0,0 +1,5 @@
import AbstractChromeStorageService from "./abstractChromeStorageApi.service";
export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
protected chromeStorageApi: any = (chrome.storage as any).session;
}

View File

@ -0,0 +1,20 @@
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { AbstractKeyGenerationService } from "./abstractions/abstractKeyGeneration.service";
export class KeyGenerationService implements AbstractKeyGenerationService {
constructor(private cryptoFunctionService: CryptoFunctionService) {}
async makeEphemeralKey(numBytes = 16): Promise<SymmetricCryptoKey> {
const keyMaterial = await this.cryptoFunctionService.randomBytes(numBytes);
const key = await this.cryptoFunctionService.hkdf(
keyMaterial,
"bitwarden-ephemeral",
"ephemeral",
64,
"sha256"
);
return new SymmetricCryptoKey(key);
}
}

View File

@ -0,0 +1,308 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { EncryptService } from "@bitwarden/common/src/services/encrypt.service";
import BrowserLocalStorageService from "./browserLocalStorage.service";
import BrowserMemoryStorageService from "./browserMemoryStorage.service";
import { KeyGenerationService } from "./keyGeneration.service";
import { LocalBackedSessionStorageService } from "./localBackedSessionStorage.service";
describe("Browser Session Storage Service", () => {
let encryptService: SubstituteOf<EncryptService>;
let keyGenerationService: SubstituteOf<KeyGenerationService>;
let cache: Map<string, any>;
const testObj = { a: 1, b: 2 };
let localStorage: BrowserLocalStorageService;
let sessionStorage: BrowserMemoryStorageService;
const key = new SymmetricCryptoKey(
Utils.fromUtf8ToArray("00000000000000000000000000000000").buffer
);
let getSessionKeySpy: jest.SpyInstance;
const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input));
let sut: LocalBackedSessionStorageService;
beforeEach(() => {
encryptService = Substitute.for();
keyGenerationService = Substitute.for();
sut = new LocalBackedSessionStorageService(encryptService, keyGenerationService);
cache = sut["cache"];
localStorage = sut["localStorage"];
sessionStorage = sut["sessionStorage"];
getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey");
getSessionKeySpy.mockResolvedValue(key);
});
it("should exist", () => {
expect(sut).toBeInstanceOf(LocalBackedSessionStorageService);
});
describe("get", () => {
it("should return from cache", async () => {
cache.set("test", testObj);
const result = await sut.get("test");
expect(result).toStrictEqual(testObj);
});
describe("not in cache", () => {
const session = { test: testObj };
beforeEach(() => {
jest.spyOn(sut, "getSessionEncKey").mockResolvedValue(key);
});
describe("no session retrieved", () => {
let result: any;
let spy: jest.SpyInstance;
beforeEach(async () => {
spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
result = await sut.get("test");
});
it("should grab from session if not in cache", async () => {
expect(spy).toHaveBeenCalledWith(key);
});
it("should return null if session is null", async () => {
expect(result).toBeNull();
});
});
describe("session retrieved from storage", () => {
beforeEach(() => {
jest.spyOn(sut, "getLocalSession").mockResolvedValue(session);
});
it("should return null if session does not have the key", async () => {
const result = await sut.get("DNE");
expect(result).toBeNull();
});
it("should return the value retrieved from session", async () => {
const result = await sut.get("test");
expect(result).toEqual(session.test);
});
it("should set retrieved values in cache", async () => {
await sut.get("test");
expect(cache.has("test")).toBe(true);
expect(cache.get("test")).toEqual(session.test);
});
});
});
});
describe("has", () => {
it("should be false if `get` returns null", async () => {
const spy = jest.spyOn(sut, "get");
spy.mockResolvedValue(null);
expect(await sut.has("test")).toBe(false);
expect(spy).toHaveBeenCalledWith("test");
});
it("should be true if `get` returns non-null", async () => {
const spy = jest.spyOn(sut, "get");
spy.mockResolvedValue({});
expect(await sut.has("test")).toBe(true);
expect(spy).toHaveBeenCalledWith("test");
});
});
describe("remove", () => {
it("should save null", async () => {
const spy = jest.spyOn(sut, "save");
spy.mockResolvedValue(null);
await sut.remove("test");
expect(spy).toHaveBeenCalledWith("test", null);
});
});
describe("save", () => {
describe("caching", () => {
beforeEach(() => {
jest.spyOn(localStorage, "get").mockResolvedValue(null);
jest.spyOn(sessionStorage, "get").mockResolvedValue(null);
jest.spyOn(localStorage, "save").mockResolvedValue();
jest.spyOn(sessionStorage, "save").mockResolvedValue();
});
it("should remove key from cache if value is null", async () => {
cache.set("test", {});
const deleteSpy = jest.spyOn(cache, "delete");
expect(cache.has("test")).toBe(true);
await sut.save("test", null);
expect(cache.has("test")).toBe(false);
expect(deleteSpy).toHaveBeenCalledWith("test");
});
it("should set cache if value is non-null", async () => {
expect(cache.has("test")).toBe(false);
const setSpy = jest.spyOn(cache, "set");
await sut.save("test", testObj);
expect(cache.get("test")).toBe(testObj);
expect(setSpy).toHaveBeenCalledWith("test", testObj);
});
});
describe("local storing", () => {
let setSpy: jest.SpyInstance;
beforeEach(() => {
setSpy = jest.spyOn(sut, "setLocalSession").mockResolvedValue();
});
it("should store a new session", async () => {
jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
await sut.save("test", testObj);
expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key);
});
it("should update an existing session", async () => {
const existingObj = { test: testObj };
jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj);
await sut.save("test2", testObj);
expect(setSpy).toHaveBeenCalledWith({ test2: testObj, ...existingObj }, key);
});
it("should overwrite an existing item in session", async () => {
const existingObj = { test: {} };
jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj);
await sut.save("test", testObj);
expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key);
});
});
});
describe("getSessionKey", () => {
beforeEach(() => {
getSessionKeySpy.mockRestore();
});
it("should return the stored symmetric crypto key", async () => {
jest.spyOn(sessionStorage, "get").mockResolvedValue({ ...key });
const result = await sut.getSessionEncKey();
expect(result).toStrictEqual(key);
});
describe("new key creation", () => {
beforeEach(() => {
jest.spyOn(sessionStorage, "get").mockResolvedValue(null);
keyGenerationService.makeEphemeralKey().resolves(key);
jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
});
it("should create a symmetric crypto key", async () => {
const result = await sut.getSessionEncKey();
expect(result).toStrictEqual(key);
keyGenerationService.received(1).makeEphemeralKey();
});
it("should store a symmetric crypto key if it makes one", async () => {
const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
await sut.getSessionEncKey();
expect(spy).toBeCalledWith(key);
});
});
});
describe("getLocalSession", () => {
it("should return null if session is null", async () => {
const spy = jest.spyOn(localStorage, "get").mockResolvedValue(null);
const result = await sut.getLocalSession(key);
expect(result).toBeNull();
expect(spy).toBeCalledWith("session");
});
describe("non-null sessions", () => {
const session = { test: "test" };
const encSession = new EncString(JSON.stringify(session));
const decryptedSession = JSON.stringify(session);
beforeEach(() => {
jest.spyOn(localStorage, "get").mockResolvedValue(encSession.encryptedString);
});
it("should decrypt returned sessions", async () => {
encryptService.decryptToUtf8(encSession, key).resolves(decryptedSession);
await sut.getLocalSession(key);
encryptService.received(1).decryptToUtf8(encSession, key);
});
it("should parse session", async () => {
encryptService.decryptToUtf8(encSession, key).resolves(decryptedSession);
const result = await sut.getLocalSession(key);
expect(result).toEqual(session);
});
it("should remove state if decryption fails", async () => {
encryptService.decryptToUtf8(Arg.any(), Arg.any()).resolves(null);
const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
const removeLocalSessionSpy = jest.spyOn(localStorage, "remove").mockResolvedValue();
const result = await sut.getLocalSession(key);
expect(result).toBeNull();
expect(setSessionEncKeySpy).toHaveBeenCalledWith(null);
expect(removeLocalSessionSpy).toHaveBeenCalledWith("session");
});
});
});
describe("setLocalSession", () => {
const testSession = { test: "a" };
const testJSON = JSON.stringify(testSession);
it("should encrypt a stringified session", async () => {
encryptService.encrypt(Arg.any(), Arg.any()).mimicks(mockEnc);
jest.spyOn(localStorage, "save").mockResolvedValue();
await sut.setLocalSession(testSession, key);
encryptService.received(1).encrypt(testJSON, key);
});
it("should remove local session if null", async () => {
encryptService.encrypt(Arg.any(), Arg.any()).resolves(null);
const spy = jest.spyOn(localStorage, "remove").mockResolvedValue();
await sut.setLocalSession(null, key);
expect(spy).toHaveBeenCalledWith("session");
});
it("should save encrypted string", async () => {
encryptService.encrypt(Arg.any(), Arg.any()).mimicks(mockEnc);
const spy = jest.spyOn(localStorage, "save").mockResolvedValue();
await sut.setLocalSession(testSession, key);
expect(spy).toHaveBeenCalledWith("session", (await mockEnc(testJSON)).encryptedString);
});
});
describe("setSessionKey", () => {
it("should remove if null", async () => {
const spy = jest.spyOn(sessionStorage, "remove").mockResolvedValue();
await sut.setSessionEncKey(null);
expect(spy).toHaveBeenCalledWith("localEncryptionKey");
});
it("should save key when not null", async () => {
const spy = jest.spyOn(sessionStorage, "save").mockResolvedValue();
await sut.setSessionEncKey(key);
expect(spy).toHaveBeenCalledWith("localEncryptionKey", key);
});
});
});

View File

@ -0,0 +1,107 @@
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { AbstractKeyGenerationService } from "./abstractions/abstractKeyGeneration.service";
import BrowserLocalStorageService from "./browserLocalStorage.service";
import BrowserMemoryStorageService from "./browserMemoryStorage.service";
const keys = {
encKey: "localEncryptionKey",
sessionKey: "session",
};
export class LocalBackedSessionStorageService extends AbstractStorageService {
private cache = new Map<string, any>();
private localStorage = new BrowserLocalStorageService();
private sessionStorage = new BrowserMemoryStorageService();
constructor(
private encryptService: AbstractEncryptService,
private keyGenerationService: AbstractKeyGenerationService
) {
super();
}
async get<T>(key: string): Promise<T> {
if (this.cache.has(key)) {
return this.cache.get(key);
}
const session = await this.getLocalSession(await this.getSessionEncKey());
if (session == null || !Object.keys(session).includes(key)) {
return null;
}
this.cache.set(key, session[key]);
return this.cache.get(key);
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
async save(key: string, obj: any): Promise<void> {
if (obj == null) {
this.cache.delete(key);
} else {
this.cache.set(key, obj);
}
const sessionEncKey = await this.getSessionEncKey();
const localSession = (await this.getLocalSession(sessionEncKey)) ?? {};
localSession[key] = obj;
await this.setLocalSession(localSession, sessionEncKey);
}
async remove(key: string): Promise<void> {
await this.save(key, null);
}
async getLocalSession(encKey: SymmetricCryptoKey): Promise<any> {
const local = await this.localStorage.get<string>(keys.sessionKey);
if (local == null) {
return null;
}
const sessionJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey);
if (sessionJson == null) {
// Error with decryption -- session is lost, delete state and key and start over
await this.setSessionEncKey(null);
await this.localStorage.remove(keys.sessionKey);
return null;
}
return JSON.parse(sessionJson);
}
async setLocalSession(session: any, key: SymmetricCryptoKey) {
const jsonSession = JSON.stringify(session);
const encSession = await this.encryptService.encrypt(jsonSession, key);
if (encSession == null) {
return await this.localStorage.remove(keys.sessionKey);
}
await this.localStorage.save(keys.sessionKey, encSession.encryptedString);
}
async getSessionEncKey(): Promise<SymmetricCryptoKey> {
let storedKey = (await this.sessionStorage.get(keys.encKey)) as SymmetricCryptoKey;
if (storedKey == null || Object.keys(storedKey).length == 0) {
storedKey = await this.keyGenerationService.makeEphemeralKey();
await this.setSessionEncKey(storedKey);
}
return SymmetricCryptoKey.initFromJson(
Object.create(SymmetricCryptoKey.prototype, Object.getOwnPropertyDescriptors(storedKey))
);
}
async setSessionEncKey(input: SymmetricCryptoKey): Promise<void> {
if (input == null) {
await this.sessionStorage.remove(keys.encKey);
} else {
await this.sessionStorage.save(keys.encKey, input);
}
}
}

View File

@ -0,0 +1,109 @@
import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SendType } from "@bitwarden/common/enums/sendType";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
import { State } from "@bitwarden/common/models/domain/state";
import { SendView } from "@bitwarden/common/models/view/sendView";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { Account } from "../models/account";
import { BrowserComponentState } from "../models/browserComponentState";
import { BrowserGroupingsComponentState } from "../models/browserGroupingsComponentState";
import { BrowserSendComponentState } from "../models/browserSendComponentState";
import { StateService } from "./state.service";
describe("Browser State Service", () => {
let secureStorageService: SubstituteOf<AbstractStorageService>;
let diskStorageService: SubstituteOf<AbstractStorageService>;
let memoryStorageService: SubstituteOf<AbstractStorageService>;
let logService: SubstituteOf<LogService>;
let stateMigrationService: SubstituteOf<StateMigrationService>;
let stateFactory: SubstituteOf<StateFactory<GlobalState, Account>>;
let useAccountCache: boolean;
let state: State<GlobalState, Account>;
const userId = "userId";
let sut: StateService;
beforeEach(() => {
secureStorageService = Substitute.for();
diskStorageService = Substitute.for();
memoryStorageService = Substitute.for();
logService = Substitute.for();
stateMigrationService = Substitute.for();
stateFactory = Substitute.for();
useAccountCache = true;
state = new State(new GlobalState());
state.accounts[userId] = new Account({
profile: { userId: userId },
});
state.activeUserId = userId;
const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state)));
memoryStorageService.get("state").mimicks(stateGetter);
sut = new StateService(
diskStorageService,
secureStorageService,
memoryStorageService,
logService,
stateMigrationService,
stateFactory,
useAccountCache
);
});
describe("getBrowserGroupingComponentState", () => {
it("should return a BrowserGroupingsComponentState", async () => {
state.accounts[userId].groupings = new BrowserGroupingsComponentState();
const actual = await sut.getBrowserGroupingComponentState();
expect(actual).toBeInstanceOf(BrowserGroupingsComponentState);
});
});
describe("getBrowserCipherComponentState", () => {
it("should return a BrowserComponentState", async () => {
const componentState = new BrowserComponentState();
componentState.scrollY = 0;
componentState.searchText = "test";
state.accounts[userId].ciphers = componentState;
const actual = await sut.getBrowserCipherComponentState();
expect(actual).toStrictEqual(componentState);
});
});
describe("getBrowserSendComponentState", () => {
it("should return a BrowserSendComponentState", async () => {
const sendState = new BrowserSendComponentState();
sendState.sends = [new SendView(), new SendView()];
sendState.typeCounts = new Map<SendType, number>([
[SendType.File, 3],
[SendType.Text, 5],
]);
state.accounts[userId].send = sendState;
const actual = await sut.getBrowserSendComponentState();
expect(actual).toBeInstanceOf(BrowserSendComponentState);
expect(actual).toMatchObject(sendState);
});
});
describe("getBrowserSendTypeComponentState", () => {
it("should return a BrowserComponentState", async () => {
const componentState = new BrowserComponentState();
componentState.scrollY = 0;
componentState.searchText = "test";
state.accounts[userId].sendType = componentState;
const actual = await sut.getBrowserSendTypeComponentState();
expect(actual).toStrictEqual(componentState);
});
});
});

View File

@ -1,6 +1,9 @@
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
import { StateService as BaseStateService } from "@bitwarden/common/services/state.service";
import {
StateService as BaseStateService,
withPrototype,
} from "@bitwarden/common/services/state.service";
import { Account } from "../models/account";
import { BrowserComponentState } from "../models/browserComponentState";
@ -24,15 +27,17 @@ export class StateService
// Check that there is an account in memory before considering the user authenticated
return (
(await super.getIsAuthenticated(options)) &&
(await this.getAccount(this.defaultInMemoryOptions)) != null
(await this.getAccount(await this.defaultInMemoryOptions())) != null
);
}
@withPrototype(BrowserGroupingsComponentState)
async getBrowserGroupingComponentState(
options?: StorageOptions
): Promise<BrowserGroupingsComponentState> {
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.groupings;
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.groupings;
}
async setBrowserGroupingComponentState(
@ -40,15 +45,20 @@ export class StateService
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, this.defaultInMemoryOptions)
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.groupings = value;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
@withPrototype(BrowserComponentState)
async getBrowserCipherComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.ciphers;
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.ciphers;
}
async setBrowserCipherComponentState(
@ -56,15 +66,20 @@ export class StateService
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, this.defaultInMemoryOptions)
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.ciphers = value;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
@withPrototype(BrowserSendComponentState)
async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> {
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.send;
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.send;
}
async setBrowserSendComponentState(
@ -72,14 +87,20 @@ export class StateService
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, this.defaultInMemoryOptions)
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.send = value;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
@withPrototype(BrowserComponentState)
async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
?.sendType;
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.sendType;
}
async setBrowserSendTypeComponentState(
@ -87,9 +108,12 @@ export class StateService
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, this.defaultInMemoryOptions)
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
account.sendType = value;
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
}
}

View File

@ -0,0 +1,26 @@
// Add chrome storage api
const get = jest.fn();
const set = jest.fn();
const has = jest.fn();
const remove = jest.fn();
const QUOTA_BYTES = 10;
const getBytesInUse = jest.fn();
const clear = jest.fn();
global.chrome = {
storage: {
local: {
set,
get,
remove,
QUOTA_BYTES,
getBytesInUse,
clear,
},
session: {
set,
get,
has,
remove,
},
},
} as any;

View File

@ -11,6 +11,9 @@ if (process.env.NODE_ENV == null) {
process.env.NODE_ENV = "development";
}
const ENV = (process.env.ENV = process.env.NODE_ENV);
const manifestVersion = process.env.MANIFEST_VERSION == 3 ? 3 : 2;
console.log(`Building Manifest Version ${manifestVersion} app`);
const moduleRules = [
{
@ -72,8 +75,8 @@ const plugins = [
}),
new CopyWebpackPlugin({
patterns: [
process.env.MANIFEST_VERSION == 3
? { from: "./src/manifest.json.v3", to: "manifest.json" }
manifestVersion == 3
? { from: "./src/manifest.v3.json", to: "manifest.json" }
: "./src/manifest.json",
{ from: "./src/_locales", to: "_locales" },
{ from: "./src/images", to: "images" },
@ -123,7 +126,7 @@ const config = {
"notification/bar": "./src/notification/bar.js",
},
optimization: {
minimize: true,
minimize: ENV !== "development",
minimizer: [
new TerserPlugin({
exclude: [/content\/.*/, /notification\/.*/],

View File

@ -5,7 +5,7 @@ const { compilerOptions } = require("./tsconfig");
module.exports = {
preset: "ts-jest",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",

View File

@ -17,12 +17,14 @@ import { CipherService } from "@bitwarden/common/services/cipher.service";
import { CollectionService } from "@bitwarden/common/services/collection.service";
import { ContainerService } from "@bitwarden/common/services/container.service";
import { CryptoService } from "@bitwarden/common/services/crypto.service";
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/services/environment.service";
import { ExportService } from "@bitwarden/common/services/export.service";
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
import { FolderService } from "@bitwarden/common/services/folder.service";
import { ImportService } from "@bitwarden/common/services/import.service";
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { NoopMessagingService } from "@bitwarden/common/services/noopMessaging.service";
import { OrganizationService } from "@bitwarden/common/services/organization.service";
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
@ -61,6 +63,7 @@ export class Main {
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: NodeEnvSecureStorageService;
memoryStorageService: MemoryStorageService;
i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService;
cryptoService: CryptoService;
@ -82,6 +85,7 @@ export class Main {
exportService: ExportService;
searchService: SearchService;
cryptoFunctionService: NodeCryptoFunctionService;
encryptService: EncryptService;
authService: AuthService;
policyService: PolicyService;
program: Program;
@ -122,6 +126,7 @@ export class Main {
(level) => process.env.BITWARDENCLI_DEBUG !== "true" && level <= LogLevelType.Info
);
this.cryptoFunctionService = new NodeCryptoFunctionService();
this.encryptService = new EncryptService(this.cryptoFunctionService, this.logService, true);
this.storageService = new LowdbStorageService(this.logService, null, p, false, true);
this.secureStorageService = new NodeEnvSecureStorageService(
this.storageService,
@ -129,6 +134,8 @@ export class Main {
() => this.cryptoService
);
this.memoryStorageService = new MemoryStorageService();
this.stateMigrationService = new StateMigrationService(
this.storageService,
this.secureStorageService,
@ -138,6 +145,7 @@ export class Main {
this.stateService = new StateService(
this.storageService,
this.secureStorageService,
this.memoryStorageService,
this.logService,
this.stateMigrationService,
new StateFactory(GlobalState, Account)
@ -145,6 +153,7 @@ export class Main {
this.cryptoService = new CryptoService(
this.cryptoFunctionService,
this.encryptService,
this.platformUtilsService,
this.logService,
this.stateService

View File

@ -1,12 +1,12 @@
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { Utils } from "@bitwarden/common/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
export class NodeEnvSecureStorageService implements StorageService {
export class NodeEnvSecureStorageService implements AbstractStorageService {
constructor(
private storageService: StorageService,
private storageService: AbstractStorageService,
private logService: LogService,
private cryptoService: () => CryptoService
) {}

View File

@ -8,8 +8,10 @@ import {
CLIENT_TYPE,
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
MEMORY_STORAGE,
} from "@bitwarden/angular/services/jslib-services.module";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
@ -23,11 +25,12 @@ import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service";
import { ClientType } from "@bitwarden/common/enums/clientType";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { SystemService } from "@bitwarden/common/services/system.service";
import { ElectronCryptoService } from "@bitwarden/electron/services/electronCrypto.service";
import { ElectronLogService } from "@bitwarden/electron/services/electronLog.service";
@ -96,13 +99,15 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
useClass: ElectronRendererMessagingService,
deps: [BroadcasterServiceAbstraction],
},
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
{ provide: AbstractStorageService, useClass: ElectronRendererStorageService },
{ provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService },
{ provide: MEMORY_STORAGE, useClass: MemoryStorageService },
{
provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService,
deps: [
CryptoFunctionServiceAbstraction,
AbstractEncryptService,
PlatformUtilsServiceAbstraction,
LogServiceAbstraction,
StateServiceAbstraction,
@ -123,8 +128,9 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
provide: StateServiceAbstraction,
useClass: StateService,
deps: [
StorageServiceAbstraction,
AbstractStorageService,
SECURE_STORAGE,
MEMORY_STORAGE,
LogService,
StateMigrationServiceAbstraction,
STATE_FACTORY,

View File

@ -4,6 +4,7 @@ import { app } from "electron";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { StateService } from "@bitwarden/common/services/state.service";
import { ElectronLogService } from "@bitwarden/electron/services/electronLog.service";
import { ElectronMainMessagingService } from "@bitwarden/electron/services/electronMainMessaging.service";
@ -25,6 +26,7 @@ export class Main {
logService: ElectronLogService;
i18nService: I18nService;
storageService: ElectronStorageService;
memoryStorageService: MemoryStorageService;
messagingService: ElectronMainMessagingService;
stateService: StateService;
desktopCredentialStorageListener: DesktopCredentialStorageListener;
@ -74,6 +76,7 @@ export class Main {
storageDefaults["global.vaultTimeout"] = -1;
storageDefaults["global.vaultTimeoutAction"] = "lock";
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
this.memoryStorageService = new MemoryStorageService();
// TODO: this state service will have access to on disk storage, but not in memory storage.
// If we could get this to work using the stateService singleton that the rest of the app uses we could save
@ -81,6 +84,7 @@ export class Main {
this.stateService = new StateService(
this.storageService,
null,
this.memoryStorageService,
this.logService,
null,
new StateFactory(GlobalState, Account),

View File

@ -8,6 +8,7 @@ import {
STATE_SERVICE_USE_CACHE,
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
MEMORY_STORAGE,
} from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
@ -23,9 +24,10 @@ import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { ImportService } from "@bitwarden/common/services/import.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
import { Account } from "../../models/account";
@ -33,7 +35,6 @@ import { GlobalState } from "../../models/globalState";
import { BroadcasterMessagingService } from "../../services/broadcasterMessaging.service";
import { HtmlStorageService } from "../../services/htmlStorage.service";
import { I18nService } from "../../services/i18n.service";
import { MemoryStorageService } from "../../services/memoryStorage.service";
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
import { StateService } from "../../services/state.service";
import { StateMigrationService } from "../../services/stateMigration.service";
@ -77,13 +78,17 @@ import { RouterService } from "./router.service";
useClass: I18nService,
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY],
},
{ provide: StorageServiceAbstraction, useClass: HtmlStorageService },
{ provide: AbstractStorageService, useClass: HtmlStorageService },
{
provide: SECURE_STORAGE,
// TODO: platformUtilsService.isDev has a helper for this, but using that service here results in a circular dependency.
// We have a tech debt item in the backlog to break up platformUtilsService, but in the meantime simply checking the environement here is less cumbersome.
useClass: process.env.NODE_ENV === "development" ? HtmlStorageService : MemoryStorageService,
},
{
provide: MEMORY_STORAGE,
useClass: MemoryStorageService,
},
{
provide: PlatformUtilsServiceAbstraction,
useClass: WebPlatformUtilsService,
@ -106,14 +111,15 @@ import { RouterService } from "./router.service";
{
provide: StateMigrationServiceAbstraction,
useClass: StateMigrationService,
deps: [StorageServiceAbstraction, SECURE_STORAGE, STATE_FACTORY],
deps: [AbstractStorageService, SECURE_STORAGE, STATE_FACTORY],
},
{
provide: StateServiceAbstraction,
useClass: StateService,
deps: [
StorageServiceAbstraction,
AbstractStorageService,
SECURE_STORAGE,
MEMORY_STORAGE,
LogService,
StateMigrationServiceAbstraction,
STATE_FACTORY,

View File

@ -1,11 +1,11 @@
import { Injectable } from "@angular/core";
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { HtmlStorageLocation } from "@bitwarden/common/enums/htmlStorageLocation";
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
@Injectable()
export class HtmlStorageService implements StorageService {
export class HtmlStorageService implements AbstractStorageService {
get defaultOptions(): StorageOptions {
return { htmlStorageLocation: HtmlStorageLocation.Session };
}

View File

@ -37,7 +37,7 @@ export class StateService
}
async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getEncryptedCiphers(options);
}
@ -45,14 +45,14 @@ export class StateService
value: { [id: string]: CipherData },
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.setEncryptedCiphers(value, options);
}
async getEncryptedCollections(
options?: StorageOptions
): Promise<{ [id: string]: CollectionData }> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getEncryptedCollections(options);
}
@ -60,12 +60,12 @@ export class StateService
value: { [id: string]: CollectionData },
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.setEncryptedCollections(value, options);
}
async getEncryptedFolders(options?: StorageOptions): Promise<{ [id: string]: FolderData }> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getEncryptedFolders(options);
}
@ -73,12 +73,12 @@ export class StateService
value: { [id: string]: FolderData },
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.setEncryptedFolders(value, options);
}
async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getEncryptedSends(options);
}
@ -86,17 +86,17 @@ export class StateService
value: { [id: string]: SendData },
options?: StorageOptions
): Promise<void> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.setEncryptedSends(value, options);
}
override async getLastSync(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.getLastSync(options);
}
override async setLastSync(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
return await super.setLastSync(value, options);
}
}

View File

@ -36,20 +36,25 @@
"webpack://?:*/*": "${workspaceFolder}/*",
"webpack://@bitwarden/cli/*": "${workspaceFolder}/*"
}
},
"jest.disabledWorkspaceFolders": [
"browser",
"cli",
"desktop",
"libs",
"web vault",
"web vault (bit)",
"desktop"
],
"jest.jestCommandLine": "npx jest",
"angular.enable-strict-mode-prompt": false
},
"jest.disabledWorkspaceFolders": [
"root",
"web vault",
"web vault (bit)",
"desktop"
],
"jest.jestCommandLine": "npx jest"
},
"extensions": {
"recommendations": [
"orta.vscode-jest",
"extensions": {
"recommendations": [
"orta.vscode-jest",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"Angular.ng-template"
],
}
}

View File

@ -7,7 +7,7 @@ module.exports = {
displayName: "libs/angular tests",
preset: "jest-preset-angular",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",

View File

@ -2,6 +2,7 @@ import { InjectionToken, Injector, LOCALE_ID, NgModule } from "@angular/core";
import { ThemingService } from "@bitwarden/angular/services/theming/theming.service";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
@ -32,7 +33,7 @@ import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstrac
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync.service";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
@ -51,6 +52,7 @@ import { CipherService } from "@bitwarden/common/services/cipher.service";
import { CollectionService } from "@bitwarden/common/services/collection.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { CryptoService } from "@bitwarden/common/services/crypto.service";
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/services/environment.service";
import { EventService } from "@bitwarden/common/services/event.service";
import { ExportService } from "@bitwarden/common/services/export.service";
@ -86,7 +88,8 @@ import { PasswordRepromptService } from "./passwordReprompt.service";
import { ValidationService } from "./validation.service";
export const WINDOW = new InjectionToken<Window>("WINDOW");
export const SECURE_STORAGE = new InjectionToken<StorageServiceAbstraction>("SECURE_STORAGE");
export const MEMORY_STORAGE = new InjectionToken<AbstractStorageService>("MEMORY_STORAGE");
export const SECURE_STORAGE = new InjectionToken<AbstractStorageService>("SECURE_STORAGE");
export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
export const LOGOUT_CALLBACK = new InjectionToken<(expired: boolean, userId?: string) => void>(
@ -142,7 +145,7 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
{
provide: AppIdServiceAbstraction,
useClass: AppIdService,
deps: [StorageServiceAbstraction],
deps: [AbstractStorageService],
},
{
provide: AuditServiceAbstraction,
@ -233,6 +236,7 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
useClass: CryptoService,
deps: [
CryptoFunctionServiceAbstraction,
AbstractEncryptService,
PlatformUtilsServiceAbstraction,
LogService,
StateServiceAbstraction,
@ -315,8 +319,9 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
provide: StateServiceAbstraction,
useClass: StateService,
deps: [
StorageServiceAbstraction,
AbstractStorageService,
SECURE_STORAGE,
MEMORY_STORAGE,
LogService,
StateMigrationServiceAbstraction,
STATE_FACTORY,
@ -326,7 +331,7 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
{
provide: StateMigrationServiceAbstraction,
useClass: StateMigrationService,
deps: [StorageServiceAbstraction, SECURE_STORAGE, STATE_FACTORY],
deps: [AbstractStorageService, SECURE_STORAGE, STATE_FACTORY],
},
{
provide: ExportServiceAbstraction,
@ -362,6 +367,11 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
useClass: WebCryptoFunctionService,
deps: [WINDOW],
},
{
provide: AbstractEncryptService,
useClass: EncryptService,
deps: [CryptoFunctionServiceAbstraction, LogService, true], // Log mac failures = true
},
{
provide: EventServiceAbstraction,
useClass: EventService,

View File

@ -8,7 +8,7 @@ module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",

View File

@ -0,0 +1,82 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { Account } from "@bitwarden/common/models/domain/account";
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
import { State } from "@bitwarden/common/models/domain/state";
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { StateService } from "@bitwarden/common/services/state.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
describe("Browser State Service backed by chrome.storage api", () => {
let secureStorageService: SubstituteOf<AbstractStorageService>;
let diskStorageService: SubstituteOf<AbstractStorageService>;
let memoryStorageService: SubstituteOf<AbstractStorageService>;
let logService: SubstituteOf<LogService>;
let stateMigrationService: SubstituteOf<StateMigrationService>;
let stateFactory: SubstituteOf<StateFactory<GlobalState, Account>>;
let useAccountCache: boolean;
let state: State<GlobalState, Account>;
const userId = "userId";
let sut: StateService;
beforeEach(() => {
secureStorageService = Substitute.for();
diskStorageService = Substitute.for();
memoryStorageService = Substitute.for();
logService = Substitute.for();
stateMigrationService = Substitute.for();
stateFactory = Substitute.for();
useAccountCache = true;
state = new State(new GlobalState());
const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state)));
memoryStorageService.get("state").mimicks(stateGetter);
memoryStorageService
.save("state", Arg.any(), Arg.any())
.mimicks((key: string, obj: any, options: StorageOptions) => {
return new Promise(() => {
state = obj;
});
});
sut = new StateService(
diskStorageService,
secureStorageService,
memoryStorageService,
logService,
stateMigrationService,
stateFactory,
useAccountCache
);
});
describe("account state getters", () => {
beforeEach(() => {
state.accounts[userId] = createAccount(userId);
state.activeUserId = userId;
});
describe("getCryptoMasterKey", () => {
it("should return the stored SymmetricCryptoKey", async () => {
const key = new SymmetricCryptoKey(new Uint8Array(32).buffer);
state.accounts[userId].keys.cryptoMasterKey = key;
const actual = await sut.getCryptoMasterKey();
expect(actual).toBeInstanceOf(SymmetricCryptoKey);
expect(actual).toMatchObject(key);
});
});
});
function createAccount(userId: string): Account {
return new Account({
profile: { userId: userId },
});
}
});

View File

@ -1,6 +1,6 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { StateVersion } from "@bitwarden/common/enums/stateVersion";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { Account } from "@bitwarden/common/models/domain/account";
@ -10,15 +10,15 @@ import { StateMigrationService } from "@bitwarden/common/services/stateMigration
const userId = "USER_ID";
describe("State Migration Service", () => {
let storageService: SubstituteOf<StorageService>;
let secureStorageService: SubstituteOf<StorageService>;
let storageService: SubstituteOf<AbstractStorageService>;
let secureStorageService: SubstituteOf<AbstractStorageService>;
let stateFactory: SubstituteOf<StateFactory>;
let stateMigrationService: StateMigrationService;
beforeEach(() => {
storageService = Substitute.for<StorageService>();
secureStorageService = Substitute.for<StorageService>();
storageService = Substitute.for<AbstractStorageService>();
secureStorageService = Substitute.for<AbstractStorageService>();
stateFactory = Substitute.for<StateFactory>();
stateMigrationService = new StateMigrationService(
@ -28,7 +28,7 @@ describe("State Migration Service", () => {
);
});
describe("StateVersion 3 to 4 migration", async () => {
describe("StateVersion 3 to 4 migration", () => {
beforeEach(() => {
const globalVersion3: Partial<GlobalState> = {
stateVersion: StateVersion.Three,

View 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>;
}

View File

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

View File

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

View File

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

View File

@ -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");

View File

@ -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,

View 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);
}
}
}

View File

@ -1,6 +1,6 @@
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
export class MemoryStorageService implements StorageService {
export class MemoryStorageService implements AbstractStorageService {
private store = new Map<string, any>();
get<T>(key: string): Promise<T> {

File diff suppressed because it is too large Load Diff

View File

@ -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>
) {}

View File

@ -7,7 +7,7 @@ module.exports = {
displayName: "libs/components tests",
preset: "jest-preset-angular",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",

View File

@ -6,7 +6,7 @@ module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",

View File

@ -1,3 +1,4 @@
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@ -9,11 +10,12 @@ import { CryptoService } from "@bitwarden/common/services/crypto.service";
export class ElectronCryptoService extends CryptoService {
constructor(
cryptoFunctionService: CryptoFunctionService,
encryptService: AbstractEncryptService,
platformUtilService: PlatformUtilsService,
logService: LogService,
stateService: StateService
) {
super(cryptoFunctionService, platformUtilService, logService, stateService);
super(cryptoFunctionService, encryptService, platformUtilService, logService, stateService);
}
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {

View File

@ -1,9 +1,9 @@
import { ipcRenderer } from "electron";
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
export class ElectronRendererSecureStorageService implements StorageService {
export class ElectronRendererSecureStorageService implements AbstractStorageService {
async get<T>(key: string, options?: StorageOptions): Promise<T> {
const val = ipcRenderer.sendSync("keytar", {
action: "getPassword",

View File

@ -1,8 +1,8 @@
import { ipcRenderer } from "electron";
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
export class ElectronRendererStorageService implements StorageService {
export class ElectronRendererStorageService implements AbstractStorageService {
get<T>(key: string): Promise<T> {
return ipcRenderer.invoke("storageService", {
action: "get",

View File

@ -2,13 +2,13 @@ import * as fs from "fs";
import { ipcMain } from "electron";
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
// eslint-disable-next-line
const Store = require("electron-store");
export class ElectronStorageService implements StorageService {
export class ElectronStorageService implements AbstractStorageService {
private store: any;
constructor(dir: string, defaults = {}) {

View File

@ -5,7 +5,7 @@ const { compilerOptions } = require("../shared/tsconfig.libs");
module.exports = {
preset: "ts-jest",
testMatch: ["**/+(*.)+(spec).+(ts)"],
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
collectCoverage: true,
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",

View File

@ -5,12 +5,12 @@ import * as lowdb from "lowdb";
import * as FileSync from "lowdb/adapters/FileSync";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { StorageService } from "@bitwarden/common/abstractions/storage.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
import { sequentialize } from "@bitwarden/common/misc/sequentialize";
import { Utils } from "@bitwarden/common/misc/utils";
export class LowdbStorageService implements StorageService {
export class LowdbStorageService implements AbstractStorageService {
protected dataFilePath: string;
private db: lowdb.LowdbSync<any>;
private defaults: any;