mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 07:13:32 +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:
@@ -4,7 +4,7 @@
|
|||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"allowSyntheticDefaultImports": true
|
"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/**/*"],
|
"include": ["../src/**/*", "../projects/**/*"],
|
||||||
"files": ["./typings.d.ts"]
|
"files": ["./typings.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ module.exports = {
|
|||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageReporters: ["html", "lcov"],
|
coverageReporters: ["html", "lcov"],
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||||
preset: "jest-preset-angular",
|
preset: "jest-preset-angular",
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||||
prefix: "<rootDir>/",
|
prefix: "<rootDir>/",
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
|
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack",
|
||||||
"build:watch": "webpack --watch",
|
"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": "cross-env NODE_ENV=production webpack",
|
||||||
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
|
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
|
||||||
"dist": "npm run build:prod && gulp dist",
|
"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:firefox": "npm run build:prod && gulp dist:firefox",
|
||||||
"dist:opera": "npm run build:prod && gulp dist:opera",
|
"dist:opera": "npm run build:prod && gulp dist:opera",
|
||||||
"dist:safari": "npm run build:prod && gulp dist:safari",
|
"dist:safari": "npm run build:prod && gulp dist:safari",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common
|
|||||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstractions/send.service";
|
import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstractions/send.service";
|
||||||
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.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 { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync.service";
|
||||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service";
|
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service";
|
||||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.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 { CollectionService } from "@bitwarden/common/services/collection.service";
|
||||||
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
|
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
|
||||||
import { ContainerService } from "@bitwarden/common/services/container.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 { EnvironmentService } from "@bitwarden/common/services/environment.service";
|
||||||
import { EventService } from "@bitwarden/common/services/event.service";
|
import { EventService } from "@bitwarden/common/services/event.service";
|
||||||
import { ExportService } from "@bitwarden/common/services/export.service";
|
import { ExportService } from "@bitwarden/common/services/export.service";
|
||||||
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
|
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
|
||||||
import { FolderService } from "@bitwarden/common/services/folder.service";
|
import { FolderService } from "@bitwarden/common/services/folder.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.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 { NotificationsService } from "@bitwarden/common/services/notifications.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/services/organization.service";
|
import { OrganizationService } from "@bitwarden/common/services/organization.service";
|
||||||
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.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 { StateService as StateServiceAbstraction } from "../services/abstractions/state.service";
|
||||||
import AutofillService from "../services/autofill.service";
|
import AutofillService from "../services/autofill.service";
|
||||||
import { BrowserCryptoService } from "../services/browserCrypto.service";
|
import { BrowserCryptoService } from "../services/browserCrypto.service";
|
||||||
|
import BrowserLocalStorageService from "../services/browserLocalStorage.service";
|
||||||
import BrowserMessagingService from "../services/browserMessaging.service";
|
import BrowserMessagingService from "../services/browserMessaging.service";
|
||||||
import BrowserMessagingPrivateModeBackgroundService from "../services/browserMessagingPrivateModeBackground.service";
|
import BrowserMessagingPrivateModeBackgroundService from "../services/browserMessagingPrivateModeBackground.service";
|
||||||
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
|
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
|
||||||
import BrowserStorageService from "../services/browserStorage.service";
|
|
||||||
import I18nService from "../services/i18n.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 { StateService } from "../services/state.service";
|
||||||
import { VaultFilterService } from "../services/vaultFilter.service";
|
import { VaultFilterService } from "../services/vaultFilter.service";
|
||||||
import VaultTimeoutService from "../services/vaultTimeout.service";
|
import VaultTimeoutService from "../services/vaultTimeout.service";
|
||||||
@@ -100,8 +104,9 @@ import WebRequestBackground from "./webRequest.background";
|
|||||||
|
|
||||||
export default class MainBackground {
|
export default class MainBackground {
|
||||||
messagingService: MessagingServiceAbstraction;
|
messagingService: MessagingServiceAbstraction;
|
||||||
storageService: StorageServiceAbstraction;
|
storageService: AbstractStorageService;
|
||||||
secureStorageService: StorageServiceAbstraction;
|
secureStorageService: AbstractStorageService;
|
||||||
|
memoryStorageService: AbstractStorageService;
|
||||||
i18nService: I18nServiceAbstraction;
|
i18nService: I18nServiceAbstraction;
|
||||||
platformUtilsService: PlatformUtilsServiceAbstraction;
|
platformUtilsService: PlatformUtilsServiceAbstraction;
|
||||||
logService: LogServiceAbstraction;
|
logService: LogServiceAbstraction;
|
||||||
@@ -141,6 +146,7 @@ export default class MainBackground {
|
|||||||
twoFactorService: TwoFactorServiceAbstraction;
|
twoFactorService: TwoFactorServiceAbstraction;
|
||||||
vaultFilterService: VaultFilterService;
|
vaultFilterService: VaultFilterService;
|
||||||
usernameGenerationService: UsernameGenerationServiceAbstraction;
|
usernameGenerationService: UsernameGenerationServiceAbstraction;
|
||||||
|
encryptService: EncryptService;
|
||||||
|
|
||||||
onUpdatedRan: boolean;
|
onUpdatedRan: boolean;
|
||||||
onReplacedRan: boolean;
|
onReplacedRan: boolean;
|
||||||
@@ -181,9 +187,17 @@ export default class MainBackground {
|
|||||||
this.messagingService = isPrivateMode
|
this.messagingService = isPrivateMode
|
||||||
? new BrowserMessagingPrivateModeBackgroundService()
|
? new BrowserMessagingPrivateModeBackgroundService()
|
||||||
: new BrowserMessagingService();
|
: new BrowserMessagingService();
|
||||||
this.storageService = new BrowserStorageService();
|
|
||||||
this.secureStorageService = new BrowserStorageService();
|
|
||||||
this.logService = new ConsoleLogService(false);
|
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.stateMigrationService = new StateMigrationService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
@@ -192,6 +206,7 @@ export default class MainBackground {
|
|||||||
this.stateService = new StateService(
|
this.stateService = new StateService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
|
this.memoryStorageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.stateMigrationService,
|
this.stateMigrationService,
|
||||||
new StateFactory(GlobalState, Account)
|
new StateFactory(GlobalState, Account)
|
||||||
@@ -219,9 +234,10 @@ export default class MainBackground {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.i18nService = new I18nService(BrowserApi.getUILanguage(window));
|
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.cryptoService = new BrowserCryptoService(
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
|
this.encryptService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.stateService
|
this.stateService
|
||||||
|
|||||||
@@ -64,9 +64,7 @@
|
|||||||
"unlimitedStorage",
|
"unlimitedStorage",
|
||||||
"clipboardRead",
|
"clipboardRead",
|
||||||
"clipboardWrite",
|
"clipboardWrite",
|
||||||
"idle",
|
"idle"
|
||||||
"webRequest",
|
|
||||||
"declarativeNetRequest"
|
|
||||||
],
|
],
|
||||||
"optional_permissions": ["nativeMessaging"],
|
"optional_permissions": ["nativeMessaging"],
|
||||||
"host_permissions": ["http://*/*", "https://*/*"],
|
"host_permissions": ["http://*/*", "https://*/*"],
|
||||||
@@ -30,12 +30,12 @@ export class PopupUtilsService {
|
|||||||
return this.privateMode;
|
return this.privateMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
getContentScrollY(win: Window, scrollingContainer = "content"): number {
|
getContentScrollY(win: Window, scrollingContainer = "main"): number {
|
||||||
const content = win.document.getElementsByTagName(scrollingContainer)[0];
|
const content = win.document.getElementsByTagName(scrollingContainer)[0];
|
||||||
return content.scrollTop;
|
return content.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
setContentScrollY(win: Window, scrollY: number, scrollingContainer = "content"): void {
|
setContentScrollY(win: Window, scrollY: number, scrollingContainer = "main"): void {
|
||||||
if (scrollY != null) {
|
if (scrollY != null) {
|
||||||
const content = win.document.getElementsByTagName(scrollingContainer)[0];
|
const content = win.document.getElementsByTagName(scrollingContainer)[0];
|
||||||
content.scrollTop = scrollY;
|
content.scrollTop = scrollY;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { LockGuard as BaseLockGuardService } from "@bitwarden/angular/guards/loc
|
|||||||
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/guards/unauth.guard";
|
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/guards/unauth.guard";
|
||||||
import {
|
import {
|
||||||
JslibServicesModule,
|
JslibServicesModule,
|
||||||
|
MEMORY_STORAGE,
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
} from "@bitwarden/angular/services/jslib-services.module";
|
} from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
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 { SendService } from "@bitwarden/common/abstractions/send.service";
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.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 { SyncService } from "@bitwarden/common/abstractions/sync.service";
|
||||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||||
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
||||||
@@ -185,8 +186,8 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
deps: [],
|
deps: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: StorageServiceAbstraction,
|
provide: AbstractStorageService,
|
||||||
useFactory: getBgService<StorageServiceAbstraction>("storageService"),
|
useFactory: getBgService<AbstractStorageService>("storageService"),
|
||||||
deps: [],
|
deps: [],
|
||||||
},
|
},
|
||||||
{ provide: AppIdService, useFactory: getBgService<AppIdService>("appIdService"), deps: [] },
|
{ provide: AppIdService, useFactory: getBgService<AppIdService>("appIdService"), deps: [] },
|
||||||
@@ -249,9 +250,13 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: SECURE_STORAGE,
|
provide: SECURE_STORAGE,
|
||||||
useFactory: getBgService<StorageServiceAbstraction>("secureStorageService"),
|
useFactory: getBgService<AbstractStorageService>("secureStorageService"),
|
||||||
deps: [],
|
deps: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MEMORY_STORAGE,
|
||||||
|
useFactory: getBgService<AbstractStorageService>("memoryStorageService"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useFactory: getBgService<StateServiceAbstraction>("stateService"),
|
useFactory: getBgService<StateServiceAbstraction>("stateService"),
|
||||||
|
|||||||
@@ -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 {
|
export default abstract class AbstractChromeStorageService implements AbstractStorageService {
|
||||||
private chromeStorageApi: any;
|
protected abstract chromeStorageApi: any;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.chromeStorageApi = chrome.storage.local;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get<T>(key: string): Promise<T> {
|
async get<T>(key: string): Promise<T> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -23,7 +19,7 @@ export default class BrowserStorageService implements StorageService {
|
|||||||
return (await this.get(key)) != null;
|
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) {
|
if (obj == null) {
|
||||||
// Fix safari not liking null in set
|
// Fix safari not liking null in set
|
||||||
return new Promise<void>((resolve) => {
|
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) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.chromeStorageApi.remove(key, () => {
|
this.chromeStorageApi.remove(key, () => {
|
||||||
resolve();
|
resolve();
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
|
export interface AbstractKeyGenerationService {
|
||||||
|
makeEphemeralKey(numBytes?: number): Promise<SymmetricCryptoKey>;
|
||||||
|
}
|
||||||
5
apps/browser/src/services/browserLocalStorage.service.ts
Normal file
5
apps/browser/src/services/browserLocalStorage.service.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import AbstractChromeStorageService from "./abstractChromeStorageApi.service";
|
||||||
|
|
||||||
|
export default class BrowserLocalStorageService extends AbstractChromeStorageService {
|
||||||
|
protected chromeStorageApi: any = chrome.storage.local;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import AbstractChromeStorageService from "./abstractChromeStorageApi.service";
|
||||||
|
|
||||||
|
export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
|
||||||
|
protected chromeStorageApi: any = (chrome.storage as any).session;
|
||||||
|
}
|
||||||
20
apps/browser/src/services/keyGeneration.service.ts
Normal file
20
apps/browser/src/services/keyGeneration.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
107
apps/browser/src/services/localBackedSessionStorage.service.ts
Normal file
107
apps/browser/src/services/localBackedSessionStorage.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
apps/browser/src/services/state.service.spec.ts
Normal file
109
apps/browser/src/services/state.service.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
|
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
|
||||||
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
|
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 { Account } from "../models/account";
|
||||||
import { BrowserComponentState } from "../models/browserComponentState";
|
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
|
// Check that there is an account in memory before considering the user authenticated
|
||||||
return (
|
return (
|
||||||
(await super.getIsAuthenticated(options)) &&
|
(await super.getIsAuthenticated(options)) &&
|
||||||
(await this.getAccount(this.defaultInMemoryOptions)) != null
|
(await this.getAccount(await this.defaultInMemoryOptions())) != null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@withPrototype(BrowserGroupingsComponentState)
|
||||||
async getBrowserGroupingComponentState(
|
async getBrowserGroupingComponentState(
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<BrowserGroupingsComponentState> {
|
): Promise<BrowserGroupingsComponentState> {
|
||||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
return (
|
||||||
?.groupings;
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
|
)?.groupings;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setBrowserGroupingComponentState(
|
async setBrowserGroupingComponentState(
|
||||||
@@ -40,15 +45,20 @@ export class StateService
|
|||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
account.groupings = value;
|
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> {
|
async getBrowserCipherComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
|
||||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
return (
|
||||||
?.ciphers;
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
|
)?.ciphers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setBrowserCipherComponentState(
|
async setBrowserCipherComponentState(
|
||||||
@@ -56,15 +66,20 @@ export class StateService
|
|||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
account.ciphers = value;
|
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> {
|
async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> {
|
||||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
return (
|
||||||
?.send;
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
|
)?.send;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setBrowserSendComponentState(
|
async setBrowserSendComponentState(
|
||||||
@@ -72,14 +87,20 @@ export class StateService
|
|||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
account.send = value;
|
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> {
|
async getBrowserSendTypeComponentState(options?: StorageOptions): Promise<BrowserComponentState> {
|
||||||
return (await this.getAccount(this.reconcileOptions(options, this.defaultInMemoryOptions)))
|
return (
|
||||||
?.sendType;
|
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||||
|
)?.sendType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setBrowserSendTypeComponentState(
|
async setBrowserSendTypeComponentState(
|
||||||
@@ -87,9 +108,12 @@ export class StateService
|
|||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const account = await this.getAccount(
|
const account = await this.getAccount(
|
||||||
this.reconcileOptions(options, this.defaultInMemoryOptions)
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
);
|
);
|
||||||
account.sendType = value;
|
account.sendType = value;
|
||||||
await this.saveAccount(account, this.reconcileOptions(options, this.defaultInMemoryOptions));
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultInMemoryOptions())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
apps/browser/test.setup.ts
Normal file
26
apps/browser/test.setup.ts
Normal 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;
|
||||||
@@ -11,6 +11,9 @@ if (process.env.NODE_ENV == null) {
|
|||||||
process.env.NODE_ENV = "development";
|
process.env.NODE_ENV = "development";
|
||||||
}
|
}
|
||||||
const ENV = (process.env.ENV = process.env.NODE_ENV);
|
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 = [
|
const moduleRules = [
|
||||||
{
|
{
|
||||||
@@ -72,8 +75,8 @@ const plugins = [
|
|||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin({
|
new CopyWebpackPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
process.env.MANIFEST_VERSION == 3
|
manifestVersion == 3
|
||||||
? { from: "./src/manifest.json.v3", to: "manifest.json" }
|
? { from: "./src/manifest.v3.json", to: "manifest.json" }
|
||||||
: "./src/manifest.json",
|
: "./src/manifest.json",
|
||||||
{ from: "./src/_locales", to: "_locales" },
|
{ from: "./src/_locales", to: "_locales" },
|
||||||
{ from: "./src/images", to: "images" },
|
{ from: "./src/images", to: "images" },
|
||||||
@@ -123,7 +126,7 @@ const config = {
|
|||||||
"notification/bar": "./src/notification/bar.js",
|
"notification/bar": "./src/notification/bar.js",
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: true,
|
minimize: ENV !== "development",
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new TerserPlugin({
|
new TerserPlugin({
|
||||||
exclude: [/content\/.*/, /notification\/.*/],
|
exclude: [/content\/.*/, /notification\/.*/],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { compilerOptions } = require("./tsconfig");
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
preset: "ts-jest",
|
preset: "ts-jest",
|
||||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
|
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageReporters: ["html", "lcov"],
|
coverageReporters: ["html", "lcov"],
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ import { CipherService } from "@bitwarden/common/services/cipher.service";
|
|||||||
import { CollectionService } from "@bitwarden/common/services/collection.service";
|
import { CollectionService } from "@bitwarden/common/services/collection.service";
|
||||||
import { ContainerService } from "@bitwarden/common/services/container.service";
|
import { ContainerService } from "@bitwarden/common/services/container.service";
|
||||||
import { CryptoService } from "@bitwarden/common/services/crypto.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 { EnvironmentService } from "@bitwarden/common/services/environment.service";
|
||||||
import { ExportService } from "@bitwarden/common/services/export.service";
|
import { ExportService } from "@bitwarden/common/services/export.service";
|
||||||
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
|
import { FileUploadService } from "@bitwarden/common/services/fileUpload.service";
|
||||||
import { FolderService } from "@bitwarden/common/services/folder.service";
|
import { FolderService } from "@bitwarden/common/services/folder.service";
|
||||||
import { ImportService } from "@bitwarden/common/services/import.service";
|
import { ImportService } from "@bitwarden/common/services/import.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.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 { NoopMessagingService } from "@bitwarden/common/services/noopMessaging.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/services/organization.service";
|
import { OrganizationService } from "@bitwarden/common/services/organization.service";
|
||||||
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
|
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
|
||||||
@@ -61,6 +63,7 @@ export class Main {
|
|||||||
messagingService: NoopMessagingService;
|
messagingService: NoopMessagingService;
|
||||||
storageService: LowdbStorageService;
|
storageService: LowdbStorageService;
|
||||||
secureStorageService: NodeEnvSecureStorageService;
|
secureStorageService: NodeEnvSecureStorageService;
|
||||||
|
memoryStorageService: MemoryStorageService;
|
||||||
i18nService: I18nService;
|
i18nService: I18nService;
|
||||||
platformUtilsService: CliPlatformUtilsService;
|
platformUtilsService: CliPlatformUtilsService;
|
||||||
cryptoService: CryptoService;
|
cryptoService: CryptoService;
|
||||||
@@ -82,6 +85,7 @@ export class Main {
|
|||||||
exportService: ExportService;
|
exportService: ExportService;
|
||||||
searchService: SearchService;
|
searchService: SearchService;
|
||||||
cryptoFunctionService: NodeCryptoFunctionService;
|
cryptoFunctionService: NodeCryptoFunctionService;
|
||||||
|
encryptService: EncryptService;
|
||||||
authService: AuthService;
|
authService: AuthService;
|
||||||
policyService: PolicyService;
|
policyService: PolicyService;
|
||||||
program: Program;
|
program: Program;
|
||||||
@@ -122,6 +126,7 @@ export class Main {
|
|||||||
(level) => process.env.BITWARDENCLI_DEBUG !== "true" && level <= LogLevelType.Info
|
(level) => process.env.BITWARDENCLI_DEBUG !== "true" && level <= LogLevelType.Info
|
||||||
);
|
);
|
||||||
this.cryptoFunctionService = new NodeCryptoFunctionService();
|
this.cryptoFunctionService = new NodeCryptoFunctionService();
|
||||||
|
this.encryptService = new EncryptService(this.cryptoFunctionService, this.logService, true);
|
||||||
this.storageService = new LowdbStorageService(this.logService, null, p, false, true);
|
this.storageService = new LowdbStorageService(this.logService, null, p, false, true);
|
||||||
this.secureStorageService = new NodeEnvSecureStorageService(
|
this.secureStorageService = new NodeEnvSecureStorageService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
@@ -129,6 +134,8 @@ export class Main {
|
|||||||
() => this.cryptoService
|
() => this.cryptoService
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.memoryStorageService = new MemoryStorageService();
|
||||||
|
|
||||||
this.stateMigrationService = new StateMigrationService(
|
this.stateMigrationService = new StateMigrationService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
@@ -138,6 +145,7 @@ export class Main {
|
|||||||
this.stateService = new StateService(
|
this.stateService = new StateService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
|
this.memoryStorageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.stateMigrationService,
|
this.stateMigrationService,
|
||||||
new StateFactory(GlobalState, Account)
|
new StateFactory(GlobalState, Account)
|
||||||
@@ -145,6 +153,7 @@ export class Main {
|
|||||||
|
|
||||||
this.cryptoService = new CryptoService(
|
this.cryptoService = new CryptoService(
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
|
this.encryptService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.stateService
|
this.stateService
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.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 { Utils } from "@bitwarden/common/misc/utils";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
export class NodeEnvSecureStorageService implements StorageService {
|
export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||||
constructor(
|
constructor(
|
||||||
private storageService: StorageService,
|
private storageService: AbstractStorageService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private cryptoService: () => CryptoService
|
private cryptoService: () => CryptoService
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
LOCALES_DIRECTORY,
|
LOCALES_DIRECTORY,
|
||||||
SYSTEM_LANGUAGE,
|
SYSTEM_LANGUAGE,
|
||||||
|
MEMORY_STORAGE,
|
||||||
} from "@bitwarden/angular/services/jslib-services.module";
|
} from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
|
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 { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.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 { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.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 { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service";
|
||||||
import { ClientType } from "@bitwarden/common/enums/clientType";
|
import { ClientType } from "@bitwarden/common/enums/clientType";
|
||||||
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
||||||
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
|
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 { SystemService } from "@bitwarden/common/services/system.service";
|
||||||
import { ElectronCryptoService } from "@bitwarden/electron/services/electronCrypto.service";
|
import { ElectronCryptoService } from "@bitwarden/electron/services/electronCrypto.service";
|
||||||
import { ElectronLogService } from "@bitwarden/electron/services/electronLog.service";
|
import { ElectronLogService } from "@bitwarden/electron/services/electronLog.service";
|
||||||
@@ -96,13 +99,15 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
|||||||
useClass: ElectronRendererMessagingService,
|
useClass: ElectronRendererMessagingService,
|
||||||
deps: [BroadcasterServiceAbstraction],
|
deps: [BroadcasterServiceAbstraction],
|
||||||
},
|
},
|
||||||
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
|
{ provide: AbstractStorageService, useClass: ElectronRendererStorageService },
|
||||||
{ provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService },
|
{ provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService },
|
||||||
|
{ provide: MEMORY_STORAGE, useClass: MemoryStorageService },
|
||||||
{
|
{
|
||||||
provide: CryptoServiceAbstraction,
|
provide: CryptoServiceAbstraction,
|
||||||
useClass: ElectronCryptoService,
|
useClass: ElectronCryptoService,
|
||||||
deps: [
|
deps: [
|
||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
|
AbstractEncryptService,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
LogServiceAbstraction,
|
LogServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
@@ -123,8 +128,9 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
|||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useClass: StateService,
|
useClass: StateService,
|
||||||
deps: [
|
deps: [
|
||||||
StorageServiceAbstraction,
|
AbstractStorageService,
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
|
MEMORY_STORAGE,
|
||||||
LogService,
|
LogService,
|
||||||
StateMigrationServiceAbstraction,
|
StateMigrationServiceAbstraction,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { app } from "electron";
|
|||||||
|
|
||||||
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
||||||
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
|
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 { StateService } from "@bitwarden/common/services/state.service";
|
||||||
import { ElectronLogService } from "@bitwarden/electron/services/electronLog.service";
|
import { ElectronLogService } from "@bitwarden/electron/services/electronLog.service";
|
||||||
import { ElectronMainMessagingService } from "@bitwarden/electron/services/electronMainMessaging.service";
|
import { ElectronMainMessagingService } from "@bitwarden/electron/services/electronMainMessaging.service";
|
||||||
@@ -25,6 +26,7 @@ export class Main {
|
|||||||
logService: ElectronLogService;
|
logService: ElectronLogService;
|
||||||
i18nService: I18nService;
|
i18nService: I18nService;
|
||||||
storageService: ElectronStorageService;
|
storageService: ElectronStorageService;
|
||||||
|
memoryStorageService: MemoryStorageService;
|
||||||
messagingService: ElectronMainMessagingService;
|
messagingService: ElectronMainMessagingService;
|
||||||
stateService: StateService;
|
stateService: StateService;
|
||||||
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
||||||
@@ -74,6 +76,7 @@ export class Main {
|
|||||||
storageDefaults["global.vaultTimeout"] = -1;
|
storageDefaults["global.vaultTimeout"] = -1;
|
||||||
storageDefaults["global.vaultTimeoutAction"] = "lock";
|
storageDefaults["global.vaultTimeoutAction"] = "lock";
|
||||||
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
|
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.
|
// 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
|
// 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.stateService = new StateService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
null,
|
null,
|
||||||
|
this.memoryStorageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
null,
|
null,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
LOCALES_DIRECTORY,
|
LOCALES_DIRECTORY,
|
||||||
SYSTEM_LANGUAGE,
|
SYSTEM_LANGUAGE,
|
||||||
|
MEMORY_STORAGE,
|
||||||
} from "@bitwarden/angular/services/jslib-services.module";
|
} from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
||||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.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 { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.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 { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
||||||
import { ImportService } from "@bitwarden/common/services/import.service";
|
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 { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
@@ -33,7 +35,6 @@ import { GlobalState } from "../../models/globalState";
|
|||||||
import { BroadcasterMessagingService } from "../../services/broadcasterMessaging.service";
|
import { BroadcasterMessagingService } from "../../services/broadcasterMessaging.service";
|
||||||
import { HtmlStorageService } from "../../services/htmlStorage.service";
|
import { HtmlStorageService } from "../../services/htmlStorage.service";
|
||||||
import { I18nService } from "../../services/i18n.service";
|
import { I18nService } from "../../services/i18n.service";
|
||||||
import { MemoryStorageService } from "../../services/memoryStorage.service";
|
|
||||||
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
|
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
|
||||||
import { StateService } from "../../services/state.service";
|
import { StateService } from "../../services/state.service";
|
||||||
import { StateMigrationService } from "../../services/stateMigration.service";
|
import { StateMigrationService } from "../../services/stateMigration.service";
|
||||||
@@ -77,13 +78,17 @@ import { RouterService } from "./router.service";
|
|||||||
useClass: I18nService,
|
useClass: I18nService,
|
||||||
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY],
|
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY],
|
||||||
},
|
},
|
||||||
{ provide: StorageServiceAbstraction, useClass: HtmlStorageService },
|
{ provide: AbstractStorageService, useClass: HtmlStorageService },
|
||||||
{
|
{
|
||||||
provide: SECURE_STORAGE,
|
provide: SECURE_STORAGE,
|
||||||
// TODO: platformUtilsService.isDev has a helper for this, but using that service here results in a circular dependency.
|
// 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.
|
// 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,
|
useClass: process.env.NODE_ENV === "development" ? HtmlStorageService : MemoryStorageService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MEMORY_STORAGE,
|
||||||
|
useClass: MemoryStorageService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: PlatformUtilsServiceAbstraction,
|
provide: PlatformUtilsServiceAbstraction,
|
||||||
useClass: WebPlatformUtilsService,
|
useClass: WebPlatformUtilsService,
|
||||||
@@ -106,14 +111,15 @@ import { RouterService } from "./router.service";
|
|||||||
{
|
{
|
||||||
provide: StateMigrationServiceAbstraction,
|
provide: StateMigrationServiceAbstraction,
|
||||||
useClass: StateMigrationService,
|
useClass: StateMigrationService,
|
||||||
deps: [StorageServiceAbstraction, SECURE_STORAGE, STATE_FACTORY],
|
deps: [AbstractStorageService, SECURE_STORAGE, STATE_FACTORY],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useClass: StateService,
|
useClass: StateService,
|
||||||
deps: [
|
deps: [
|
||||||
StorageServiceAbstraction,
|
AbstractStorageService,
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
|
MEMORY_STORAGE,
|
||||||
LogService,
|
LogService,
|
||||||
StateMigrationServiceAbstraction,
|
StateMigrationServiceAbstraction,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from "@angular/core";
|
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 { HtmlStorageLocation } from "@bitwarden/common/enums/htmlStorageLocation";
|
||||||
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
|
import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HtmlStorageService implements StorageService {
|
export class HtmlStorageService implements AbstractStorageService {
|
||||||
get defaultOptions(): StorageOptions {
|
get defaultOptions(): StorageOptions {
|
||||||
return { htmlStorageLocation: HtmlStorageLocation.Session };
|
return { htmlStorageLocation: HtmlStorageLocation.Session };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class StateService
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> {
|
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);
|
return await super.getEncryptedCiphers(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,14 +45,14 @@ export class StateService
|
|||||||
value: { [id: string]: CipherData },
|
value: { [id: string]: CipherData },
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
|
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||||
return await super.setEncryptedCiphers(value, options);
|
return await super.setEncryptedCiphers(value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEncryptedCollections(
|
async getEncryptedCollections(
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<{ [id: string]: CollectionData }> {
|
): Promise<{ [id: string]: CollectionData }> {
|
||||||
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
|
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||||
return await super.getEncryptedCollections(options);
|
return await super.getEncryptedCollections(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,12 +60,12 @@ export class StateService
|
|||||||
value: { [id: string]: CollectionData },
|
value: { [id: string]: CollectionData },
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
|
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||||
return await super.setEncryptedCollections(value, options);
|
return await super.setEncryptedCollections(value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEncryptedFolders(options?: StorageOptions): Promise<{ [id: string]: FolderData }> {
|
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);
|
return await super.getEncryptedFolders(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,12 +73,12 @@ export class StateService
|
|||||||
value: { [id: string]: FolderData },
|
value: { [id: string]: FolderData },
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
|
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||||
return await super.setEncryptedFolders(value, options);
|
return await super.setEncryptedFolders(value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> {
|
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);
|
return await super.getEncryptedSends(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,17 +86,17 @@ export class StateService
|
|||||||
value: { [id: string]: SendData },
|
value: { [id: string]: SendData },
|
||||||
options?: StorageOptions
|
options?: StorageOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
options = this.reconcileOptions(options, this.defaultInMemoryOptions);
|
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||||
return await super.setEncryptedSends(value, options);
|
return await super.setEncryptedSends(value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async getLastSync(options?: StorageOptions): Promise<string> {
|
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);
|
return await super.getLastSync(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async setLastSync(value: string, options?: StorageOptions): Promise<void> {
|
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);
|
return await super.setLastSync(value, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,20 +36,25 @@
|
|||||||
"webpack://?:*/*": "${workspaceFolder}/*",
|
"webpack://?:*/*": "${workspaceFolder}/*",
|
||||||
"webpack://@bitwarden/cli/*": "${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": [
|
"extensions": {
|
||||||
"root",
|
"recommendations": [
|
||||||
"web vault",
|
"orta.vscode-jest",
|
||||||
"web vault (bit)",
|
|
||||||
"desktop"
|
|
||||||
],
|
|
||||||
"jest.jestCommandLine": "npx jest"
|
|
||||||
},
|
|
||||||
"extensions": {
|
|
||||||
"recommendations": [
|
|
||||||
"orta.vscode-jest",
|
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode"
|
"esbenp.prettier-vscode",
|
||||||
|
"Angular.ng-template"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module.exports = {
|
|||||||
displayName: "libs/angular tests",
|
displayName: "libs/angular tests",
|
||||||
preset: "jest-preset-angular",
|
preset: "jest-preset-angular",
|
||||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
|
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageReporters: ["html", "lcov"],
|
coverageReporters: ["html", "lcov"],
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { InjectionToken, Injector, LOCALE_ID, NgModule } from "@angular/core";
|
|||||||
|
|
||||||
import { ThemingService } from "@bitwarden/angular/services/theming/theming.service";
|
import { ThemingService } from "@bitwarden/angular/services/theming/theming.service";
|
||||||
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
|
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 { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
|
||||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.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 { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.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 { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync.service";
|
||||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service";
|
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service";
|
||||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.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 { CollectionService } from "@bitwarden/common/services/collection.service";
|
||||||
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
|
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
|
||||||
import { CryptoService } from "@bitwarden/common/services/crypto.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 { EnvironmentService } from "@bitwarden/common/services/environment.service";
|
||||||
import { EventService } from "@bitwarden/common/services/event.service";
|
import { EventService } from "@bitwarden/common/services/event.service";
|
||||||
import { ExportService } from "@bitwarden/common/services/export.service";
|
import { ExportService } from "@bitwarden/common/services/export.service";
|
||||||
@@ -86,7 +88,8 @@ import { PasswordRepromptService } from "./passwordReprompt.service";
|
|||||||
import { ValidationService } from "./validation.service";
|
import { ValidationService } from "./validation.service";
|
||||||
|
|
||||||
export const WINDOW = new InjectionToken<Window>("WINDOW");
|
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_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
|
||||||
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
|
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
|
||||||
export const LOGOUT_CALLBACK = new InjectionToken<(expired: boolean, userId?: string) => void>(
|
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,
|
provide: AppIdServiceAbstraction,
|
||||||
useClass: AppIdService,
|
useClass: AppIdService,
|
||||||
deps: [StorageServiceAbstraction],
|
deps: [AbstractStorageService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AuditServiceAbstraction,
|
provide: AuditServiceAbstraction,
|
||||||
@@ -233,6 +236,7 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
|
|||||||
useClass: CryptoService,
|
useClass: CryptoService,
|
||||||
deps: [
|
deps: [
|
||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
|
AbstractEncryptService,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
@@ -315,8 +319,9 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
|
|||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useClass: StateService,
|
useClass: StateService,
|
||||||
deps: [
|
deps: [
|
||||||
StorageServiceAbstraction,
|
AbstractStorageService,
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
|
MEMORY_STORAGE,
|
||||||
LogService,
|
LogService,
|
||||||
StateMigrationServiceAbstraction,
|
StateMigrationServiceAbstraction,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
@@ -326,7 +331,7 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
|
|||||||
{
|
{
|
||||||
provide: StateMigrationServiceAbstraction,
|
provide: StateMigrationServiceAbstraction,
|
||||||
useClass: StateMigrationService,
|
useClass: StateMigrationService,
|
||||||
deps: [StorageServiceAbstraction, SECURE_STORAGE, STATE_FACTORY],
|
deps: [AbstractStorageService, SECURE_STORAGE, STATE_FACTORY],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ExportServiceAbstraction,
|
provide: ExportServiceAbstraction,
|
||||||
@@ -362,6 +367,11 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
|
|||||||
useClass: WebCryptoFunctionService,
|
useClass: WebCryptoFunctionService,
|
||||||
deps: [WINDOW],
|
deps: [WINDOW],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AbstractEncryptService,
|
||||||
|
useClass: EncryptService,
|
||||||
|
deps: [CryptoFunctionServiceAbstraction, LogService, true], // Log mac failures = true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: EventServiceAbstraction,
|
provide: EventServiceAbstraction,
|
||||||
useClass: EventService,
|
useClass: EventService,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module.exports = {
|
|||||||
preset: "ts-jest",
|
preset: "ts-jest",
|
||||||
testEnvironment: "jsdom",
|
testEnvironment: "jsdom",
|
||||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
|
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageReporters: ["html", "lcov"],
|
coverageReporters: ["html", "lcov"],
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
|||||||
82
libs/common/spec/services/state.service.spec.ts
Normal file
82
libs/common/spec/services/state.service.spec.ts
Normal 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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
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 { StateVersion } from "@bitwarden/common/enums/stateVersion";
|
||||||
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
||||||
import { Account } from "@bitwarden/common/models/domain/account";
|
import { Account } from "@bitwarden/common/models/domain/account";
|
||||||
@@ -10,15 +10,15 @@ import { StateMigrationService } from "@bitwarden/common/services/stateMigration
|
|||||||
const userId = "USER_ID";
|
const userId = "USER_ID";
|
||||||
|
|
||||||
describe("State Migration Service", () => {
|
describe("State Migration Service", () => {
|
||||||
let storageService: SubstituteOf<StorageService>;
|
let storageService: SubstituteOf<AbstractStorageService>;
|
||||||
let secureStorageService: SubstituteOf<StorageService>;
|
let secureStorageService: SubstituteOf<AbstractStorageService>;
|
||||||
let stateFactory: SubstituteOf<StateFactory>;
|
let stateFactory: SubstituteOf<StateFactory>;
|
||||||
|
|
||||||
let stateMigrationService: StateMigrationService;
|
let stateMigrationService: StateMigrationService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
storageService = Substitute.for<StorageService>();
|
storageService = Substitute.for<AbstractStorageService>();
|
||||||
secureStorageService = Substitute.for<StorageService>();
|
secureStorageService = Substitute.for<AbstractStorageService>();
|
||||||
stateFactory = Substitute.for<StateFactory>();
|
stateFactory = Substitute.for<StateFactory>();
|
||||||
|
|
||||||
stateMigrationService = new StateMigrationService(
|
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(() => {
|
beforeEach(() => {
|
||||||
const globalVersion3: Partial<GlobalState> = {
|
const globalVersion3: Partial<GlobalState> = {
|
||||||
stateVersion: StateVersion.Three,
|
stateVersion: StateVersion.Three,
|
||||||
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";
|
import { StorageOptions } from "../models/domain/storageOptions";
|
||||||
|
|
||||||
export abstract class StorageService {
|
export abstract class AbstractStorageService {
|
||||||
get: <T>(key: string, options?: StorageOptions) => Promise<T>;
|
abstract get<T>(key: string, options?: StorageOptions): Promise<T>;
|
||||||
has: (key: string, options?: StorageOptions) => Promise<boolean>;
|
abstract has(key: string, options?: StorageOptions): Promise<boolean>;
|
||||||
save: (key: string, obj: any, options?: StorageOptions) => Promise<any>;
|
abstract save<T>(key: string, obj: T, options?: StorageOptions): Promise<void>;
|
||||||
remove: (key: string, options?: StorageOptions) => Promise<any>;
|
abstract remove(key: string, options?: StorageOptions): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { SymmetricCryptoKey } from "./symmetricCryptoKey";
|
|||||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||||
encrypted?: TEncrypted;
|
encrypted?: TEncrypted;
|
||||||
decrypted?: TDecrypted;
|
decrypted?: TDecrypted;
|
||||||
|
decryptedSerialized?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
||||||
@@ -76,6 +77,7 @@ export class AccountKeys {
|
|||||||
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
|
||||||
legacyEtmKey?: SymmetricCryptoKey;
|
legacyEtmKey?: SymmetricCryptoKey;
|
||||||
publicKey?: ArrayBuffer;
|
publicKey?: ArrayBuffer;
|
||||||
|
publicKeySerialized?: string;
|
||||||
apiKeyClientSecret?: string;
|
apiKeyClientSecret?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,4 +54,22 @@ export class SymmetricCryptoKey {
|
|||||||
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
|
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 { 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 { HtmlStorageLocation } from "../enums/htmlStorageLocation";
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
|
|
||||||
export class AppIdService implements AppIdServiceAbstraction {
|
export class AppIdService implements AppIdServiceAbstraction {
|
||||||
constructor(private storageService: StorageService) {}
|
constructor(private storageService: AbstractStorageService) {}
|
||||||
|
|
||||||
getAppId(): Promise<string> {
|
getAppId(): Promise<string> {
|
||||||
return this.makeAndGetAppId("appId");
|
return this.makeAndGetAppId("appId");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as bigInt from "big-integer";
|
import * as bigInt from "big-integer";
|
||||||
|
|
||||||
|
import { AbstractEncryptService } from "../abstractions/abstractEncrypt.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "../abstractions/crypto.service";
|
||||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
@@ -23,6 +24,7 @@ import { ProfileProviderResponse } from "../models/response/profileProviderRespo
|
|||||||
export class CryptoService implements CryptoServiceAbstraction {
|
export class CryptoService implements CryptoServiceAbstraction {
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoFunctionService: CryptoFunctionService,
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
|
private encryptService: AbstractEncryptService,
|
||||||
protected platformUtilService: PlatformUtilsService,
|
protected platformUtilService: PlatformUtilsService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected stateService: StateService
|
protected stateService: StateService
|
||||||
@@ -503,23 +505,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return this.buildEncKey(key, encKey.key);
|
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> {
|
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
|
||||||
if (plainValue == null) {
|
key = await this.getKeyForEncryption(key);
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
let plainBuf: ArrayBuffer;
|
return await this.encryptService.encrypt(plainValue, key);
|
||||||
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 encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
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> {
|
async decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise<string> {
|
||||||
return await this.aesDecryptToUtf8(
|
key = await this.getKeyForEncryption(key);
|
||||||
encString.encryptionType,
|
key = await this.resolveLegacyKey(encString.encryptionType, key);
|
||||||
encString.data,
|
return await this.encryptService.decryptToUtf8(encString, key);
|
||||||
encString.iv,
|
|
||||||
encString.mac,
|
|
||||||
key
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
async decryptFromBytes(encBuf: ArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
|
||||||
@@ -754,6 +744,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
: await this.stateService.getCryptoMasterKeyBiometric({ userId: userId });
|
: 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> {
|
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||||
const obj = new EncryptedObject();
|
const obj = new EncryptedObject();
|
||||||
obj.key = await this.getKeyForEncryption(key);
|
obj.key = await this.getKeyForEncryption(key);
|
||||||
@@ -770,43 +764,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
return obj;
|
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(
|
private async aesDecryptToBytes(
|
||||||
encType: EncryptionType,
|
encType: EncryptionType,
|
||||||
data: ArrayBuffer,
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>();
|
private store = new Map<string, any>();
|
||||||
|
|
||||||
get<T>(key: string): Promise<T> {
|
get<T>(key: string): Promise<T> {
|
||||||
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 { HtmlStorageLocation } from "../enums/htmlStorageLocation";
|
||||||
import { KdfType } from "../enums/kdfType";
|
import { KdfType } from "../enums/kdfType";
|
||||||
import { StateVersion } from "../enums/stateVersion";
|
import { StateVersion } from "../enums/stateVersion";
|
||||||
@@ -132,8 +132,8 @@ export class StateMigrationService<
|
|||||||
TAccount extends Account = Account
|
TAccount extends Account = Account
|
||||||
> {
|
> {
|
||||||
constructor(
|
constructor(
|
||||||
protected storageService: StorageService,
|
protected storageService: AbstractStorageService,
|
||||||
protected secureStorageService: StorageService,
|
protected secureStorageService: AbstractStorageService,
|
||||||
protected stateFactory: StateFactory<TGlobalState, TAccount>
|
protected stateFactory: StateFactory<TGlobalState, TAccount>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module.exports = {
|
|||||||
displayName: "libs/components tests",
|
displayName: "libs/components tests",
|
||||||
preset: "jest-preset-angular",
|
preset: "jest-preset-angular",
|
||||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
|
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageReporters: ["html", "lcov"],
|
coverageReporters: ["html", "lcov"],
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module.exports = {
|
|||||||
preset: "ts-jest",
|
preset: "ts-jest",
|
||||||
testEnvironment: "jsdom",
|
testEnvironment: "jsdom",
|
||||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
|
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageReporters: ["html", "lcov"],
|
coverageReporters: ["html", "lcov"],
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.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 {
|
export class ElectronCryptoService extends CryptoService {
|
||||||
constructor(
|
constructor(
|
||||||
cryptoFunctionService: CryptoFunctionService,
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
encryptService: AbstractEncryptService,
|
||||||
platformUtilService: PlatformUtilsService,
|
platformUtilService: PlatformUtilsService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
stateService: StateService
|
stateService: StateService
|
||||||
) {
|
) {
|
||||||
super(cryptoFunctionService, platformUtilService, logService, stateService);
|
super(cryptoFunctionService, encryptService, platformUtilService, logService, stateService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
|
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ipcRenderer } from "electron";
|
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";
|
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> {
|
async get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||||
const val = ipcRenderer.sendSync("keytar", {
|
const val = ipcRenderer.sendSync("keytar", {
|
||||||
action: "getPassword",
|
action: "getPassword",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ipcRenderer } from "electron";
|
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> {
|
get<T>(key: string): Promise<T> {
|
||||||
return ipcRenderer.invoke("storageService", {
|
return ipcRenderer.invoke("storageService", {
|
||||||
action: "get",
|
action: "get",
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import * as fs from "fs";
|
|||||||
|
|
||||||
import { ipcMain } from "electron";
|
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";
|
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const Store = require("electron-store");
|
const Store = require("electron-store");
|
||||||
|
|
||||||
export class ElectronStorageService implements StorageService {
|
export class ElectronStorageService implements AbstractStorageService {
|
||||||
private store: any;
|
private store: any;
|
||||||
|
|
||||||
constructor(dir: string, defaults = {}) {
|
constructor(dir: string, defaults = {}) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { compilerOptions } = require("../shared/tsconfig.libs");
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
preset: "ts-jest",
|
preset: "ts-jest",
|
||||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
|
setupFilesAfterEnv: ["<rootDir>/spec/test.setup.ts"],
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageReporters: ["html", "lcov"],
|
coverageReporters: ["html", "lcov"],
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import * as lowdb from "lowdb";
|
|||||||
import * as FileSync from "lowdb/adapters/FileSync";
|
import * as FileSync from "lowdb/adapters/FileSync";
|
||||||
|
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.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 { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
|
||||||
import { sequentialize } from "@bitwarden/common/misc/sequentialize";
|
import { sequentialize } from "@bitwarden/common/misc/sequentialize";
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
|
|
||||||
export class LowdbStorageService implements StorageService {
|
export class LowdbStorageService implements AbstractStorageService {
|
||||||
protected dataFilePath: string;
|
protected dataFilePath: string;
|
||||||
private db: lowdb.LowdbSync<any>;
|
private db: lowdb.LowdbSync<any>;
|
||||||
private defaults: any;
|
private defaults: any;
|
||||||
|
|||||||
Reference in New Issue
Block a user