diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4a7bbdb4ffc..03f7d466f25 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -274,6 +274,7 @@ import CommandsBackground from "./commands.background"; import IdleBackground from "./idle.background"; import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; +import { UnsupportedSecureStorageService } from "@bitwarden/common/platform/storage/secure-storage.service"; export default class MainBackground { messagingService: MessageSender; @@ -585,11 +586,12 @@ export default class MainBackground { this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); + const secureStorage = new UnsupportedSecureStorageService("no-browser-api"); + this.tokenService = new TokenService( this.singleUserStateProvider, this.globalStateProvider, - this.platformUtilsService, - this.secureStorageService, + secureStorage, this.keyGenerationService, this.encryptService, this.logService, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 24d82ab8b67..f6787170d4d 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -159,6 +159,10 @@ import { VaultFilterService } from "../../vault/services/vault-filter.service"; import { DebounceNavigationService } from "./debounce-navigation.service"; import { InitService } from "./init.service"; import { PopupCloseWarningService } from "./popup-close-warning.service"; +import { + SecureStorageService, + UnsupportedSecureStorageService, +} from "@bitwarden/common/platform/storage/secure-storage.service"; const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService @@ -596,6 +600,11 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionLoginDecryptionOptionsService, deps: [MessagingServiceAbstraction, Router], }), + safeProvider({ + provide: SecureStorageService, + useFactory: () => new UnsupportedSecureStorageService("no-browser-api"), + deps: [], + }), ]; @NgModule({ diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index d6e325fe380..0e554fd8e74 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -173,6 +173,7 @@ import { I18nService } from "../platform/services/i18n.service"; import { LowdbStorageService } from "../platform/services/lowdb-storage.service"; import { NodeApiService } from "../platform/services/node-api.service"; import { NodeEnvSecureStorageService } from "../platform/services/node-env-secure-storage.service"; +import { UnsupportedSecureStorageService } from "@bitwarden/common/platform/storage/secure-storage.service"; // Polyfills global.DOMParser = new jsdom.JSDOM().window.DOMParser; @@ -372,11 +373,14 @@ export class ServiceContainer { this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); + // TODO: CLI _DOES_ have a secure storage implementation but we report it as not supported + // in PlatformUtilsService.supportsSecureStorage + const secureStorage = new UnsupportedSecureStorageService("i-dont-know"); + this.tokenService = new TokenService( this.singleUserStateProvider, this.globalStateProvider, - this.platformUtilsService, - this.secureStorageService, + secureStorage, this.keyGenerationService, this.encryptService, this.logService, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 8568334e3e9..c7de2c30466 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -127,6 +127,11 @@ import { DesktopThemeStateService } from "./desktop-theme.service"; import { InitService } from "./init.service"; import { NativeMessagingManifestService } from "./native-messaging-manifest.service"; import { RendererCryptoFunctionService } from "./renderer-crypto-function.service"; +import { + SecureStorageService, + SupportedSecureStorageService, +} from "@bitwarden/common/platform/storage/secure-storage.service"; +import { PortableSecureStorageService } from "../../platform/services/portable-secure-storage.service"; const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); @@ -397,6 +402,17 @@ const safeProviders: SafeProvider[] = [ useClass: DesktopLoginApprovalComponentService, deps: [I18nServiceAbstraction], }), + safeProvider({ + provide: SecureStorageService, + useFactory: (secureStorage: AbstractStorageService) => { + if (ipc.platform.isWindowsPortable) { + return new PortableSecureStorageService(secureStorage); + } + + return new SupportedSecureStorageService(secureStorage); + }, + deps: [SECURE_STORAGE], + }), ]; @NgModule({ diff --git a/apps/desktop/src/platform/services/portable-secure-storage.service.ts b/apps/desktop/src/platform/services/portable-secure-storage.service.ts new file mode 100644 index 00000000000..9d7088f8618 --- /dev/null +++ b/apps/desktop/src/platform/services/portable-secure-storage.service.ts @@ -0,0 +1,19 @@ +import { Observable, of } from "rxjs"; + +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; +import { + SecureStorageService, + SupportStatus, +} from "@bitwarden/common/platform/storage/secure-storage.service"; + +export class PortableSecureStorageService implements SecureStorageService { + constructor(storageService: AbstractStorageService) { + this.support$ = of({ + type: "not-preferred", + service: storageService, + reason: "portable-desktop", + } satisfies SupportStatus); + } + + support$: Observable; +} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 8f21dfa2c8b..d1d7fbccfe0 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -120,6 +120,10 @@ import { ModalService } from "./modal.service"; import { RouterService } from "./router.service"; import { WebFileDownloadService } from "./web-file-download.service"; import { WebPlatformUtilsService } from "./web-platform-utils.service"; +import { + SecureStorageService, + UnsupportedSecureStorageService, +} from "@bitwarden/common/platform/storage/secure-storage.service"; /** * Provider definitions used in the ngModule. @@ -313,6 +317,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebLoginDecryptionOptionsService, deps: [MessagingService, RouterService, AcceptOrganizationInviteService], }), + safeProvider({ + provide: SecureStorageService, + useFactory: () => new UnsupportedSecureStorageService("no-web-api"), + deps: [], + }), ]; @NgModule({ diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3d801f5ce0a..f16252b65aa 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -335,6 +335,7 @@ import { ENV_ADDITIONAL_REGIONS, } from "./injection-tokens"; import { ModalService } from "./modal.service"; +import { SecureStorageService } from "@bitwarden/common/platform/storage/secure-storage.service"; /** * Provider definitions used in the ngModule. @@ -593,8 +594,7 @@ const safeProviders: SafeProvider[] = [ deps: [ SingleUserStateProvider, GlobalStateProvider, - PlatformUtilsServiceAbstraction, - SECURE_STORAGE, + SecureStorageService, KeyGenerationServiceAbstraction, EncryptService, LogService, diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index fa0fc8f2501..8b826293637 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -43,6 +43,11 @@ export abstract class PlatformUtilsService { abstract isSelfHost(): boolean; abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; abstract readFromClipboard(): Promise; + + /** + * @deprecated Use `SecureStorageService.support$` to check the current support status of SecureStorage + * on the current client. + */ abstract supportsSecureStorage(): boolean; abstract getAutofillKeyboardShortcut(): Promise; } diff --git a/libs/common/src/platform/storage/secure-storage.service.ts b/libs/common/src/platform/storage/secure-storage.service.ts new file mode 100644 index 00000000000..9edfa4a88d1 --- /dev/null +++ b/libs/common/src/platform/storage/secure-storage.service.ts @@ -0,0 +1,64 @@ +import { Observable, of } from "rxjs"; + +import { AbstractStorageService } from "../abstractions/storage.service"; + +export type SupportStatus = + | { + type: "supported"; + service: AbstractStorageService; + } + | { type: "not-preferred"; service: AbstractStorageService; reason: string } + | { type: "needs-configuration"; reason: string } + | { type: "not-supported"; reason: string }; + +export abstract class SecureStorageService { + /** + * Returns an observable stream of a Rust-like enum showing if secure storage is + * supported for a given platform. + * + * If the type is `supported` then the service property _should_ be used to store + * security sensitive information, particularly keys and access tokens that need + * to be available after a restart of the application. + * + * If the type is `not-preferred` then the service property should be used only for + * retrieval of existing data and clearing of data. But should not be used for storage + * of new data. This means that the a secure storage implementation exists but for some + * reason the usage of it might degrade user experience. An example of this is our + * desktop app operating in its portable mode. The system API's are available to make + * secure storage work but it makes it impossible to login on one machine, then move + * the application to another machine and unlock there. + * + * If the type is `needs-configuration` then secure storage cannot be used at all. For + * purposes of using the API this type is the same as `not-supported` but you may utilize + * this type to add a callout somewhere in the application so that the documentation can + * be linked to. This documentation can then help instruct the user on what to do so that + * they can start using secure storage. + * + * If the type is `not-supported` then secure storage is not available and cannot be used. + * Depending on the feature, then you may need to not allow a feature to be used or you + * will need to fallback to using insecure, disk based storage. + */ + support$: Observable; +} + +export class UnsupportedSecureStorageService implements SecureStorageService { + constructor(reason: string) { + this.support$ = of({ + type: "not-supported", + reason: reason, + } satisfies SupportStatus); + } + + support$: Observable; +} + +export class SupportedSecureStorageService implements SecureStorageService { + constructor(storageService: AbstractStorageService) { + this.support$ = of({ + type: "supported", + service: storageService, + } satisfies SupportStatus); + } + + support$: Observable; +}