diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json index 5113cd7d1bf..91d48309240 100644 --- a/apps/browser/config/base.json +++ b/apps/browser/config/base.json @@ -2,6 +2,7 @@ "devFlags": {}, "flags": { "showPasswordless": true, - "accountSwitching": false + "accountSwitching": false, + "sdk": true } } diff --git a/apps/browser/package.json b/apps/browser/package.json index 9d878694594..15caff9eff2 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.10.0", + "version": "2024.10.1", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d3da7ba0ba5..ec1842e1053 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -90,6 +90,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, @@ -122,6 +123,8 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; +import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; @@ -228,6 +231,7 @@ import AutofillService from "../autofill/services/autofill.service"; import { SafariApp } from "../browser/safariApp"; import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service"; import { BrowserApi } from "../platform/browser/browser-api"; +import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; /* eslint-disable no-restricted-imports */ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; @@ -245,6 +249,7 @@ import { LocalBackedSessionStorageService } from "../platform/services/local-bac import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; +import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; @@ -364,6 +369,7 @@ export default class MainBackground { syncServiceListener: SyncServiceListener; themeStateService: DefaultThemeStateService; autoSubmitLoginBackground: AutoSubmitLoginBackground; + sdkService: SdkService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -719,6 +725,16 @@ export default class MainBackground { this.stateProvider, ); + const sdkClientFactory = flagEnabled("sdk") + ? new BrowserSdkClientFactory() + : new NoopSdkClientFactory(); + this.sdkService = new DefaultSdkService( + sdkClientFactory, + this.environmentService, + this.platformUtilsService, + this.apiService, + ); + this.passwordStrengthService = new PasswordStrengthService(); this.passwordGenerationService = legacyPasswordGenerationServiceFactory( @@ -1315,6 +1331,20 @@ export default class MainBackground { await this.initOverlayAndTabsBackground(); + if (flagEnabled("sdk")) { + // Warn if the SDK for some reason can't be initialized + let supported = false; + try { + supported = await firstValueFrom(this.sdkService.supported$); + } catch (e) { + // Do nothing. + } + + if (!supported) { + this.sdkService.failedToInitialize().catch(this.logService.error); + } + } + return new Promise((resolve) => { setTimeout(async () => { await this.refreshBadge(); diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 598c6ff0286..850c5c4727a 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.10.0", + "version": "2024.10.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 5413ee5b63f..0b89a36d700 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.10.0", + "version": "2024.10.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -39,8 +39,7 @@ } ], "background": { - "service_worker": "background.js", - "type": "module" + "service_worker": "background.js" }, "action": { "default_icon": { diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts b/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts new file mode 100644 index 00000000000..d3b8f36a540 --- /dev/null +++ b/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts @@ -0,0 +1,66 @@ +import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import type { BitwardenClient } from "@bitwarden/sdk-internal"; + +import { BrowserApi } from "../../browser/browser-api"; + +// https://stackoverflow.com/a/47880734 +const supported = (() => { + try { + if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { + const module = new WebAssembly.Module( + Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), + ); + if (module instanceof WebAssembly.Module) { + return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } + } catch (e) { + // ignore + } + return false; +})(); + +// Manifest v3 does not support dynamic imports in the service worker. +if (BrowserApi.isManifestVersion(3)) { + if (supported) { + // eslint-disable-next-line no-console + console.debug("WebAssembly is supported in this environment"); + import("./wasm"); + } else { + // eslint-disable-next-line no-console + console.debug("WebAssembly is not supported in this environment"); + import("./fallback"); + } +} + +// Manifest v2 expects dynamic imports to prevent timing issues. +async function load() { + if (BrowserApi.isManifestVersion(3)) { + return; + } + + if (supported) { + // eslint-disable-next-line no-console + console.debug("WebAssembly is supported in this environment"); + await import("./wasm"); + } else { + // eslint-disable-next-line no-console + console.debug("WebAssembly is not supported in this environment"); + await import("./fallback"); + } +} + +/** + * SDK client factory with a js fallback for when WASM is not supported. + * + * Works both in popup and service worker. + */ +export class BrowserSdkClientFactory implements SdkClientFactory { + async createSdkClient( + ...args: ConstructorParameters + ): Promise { + await load(); + + return Promise.resolve((globalThis as any).init_sdk(...args)); + } +} diff --git a/apps/browser/src/platform/services/sdk/fallback.ts b/apps/browser/src/platform/services/sdk/fallback.ts new file mode 100644 index 00000000000..82d292fc9ee --- /dev/null +++ b/apps/browser/src/platform/services/sdk/fallback.ts @@ -0,0 +1,8 @@ +import * as sdk from "@bitwarden/sdk-internal"; +import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"; + +(globalThis as any).init_sdk = (...args: ConstructorParameters) => { + (sdk as any).init(wasm); + + return new sdk.BitwardenClient(...args); +}; diff --git a/apps/browser/src/platform/services/sdk/wasm.ts b/apps/browser/src/platform/services/sdk/wasm.ts new file mode 100644 index 00000000000..1977a171e23 --- /dev/null +++ b/apps/browser/src/platform/services/sdk/wasm.ts @@ -0,0 +1,8 @@ +import * as sdk from "@bitwarden/sdk-internal"; +import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; + +(globalThis as any).init_sdk = (...args: ConstructorParameters) => { + (sdk as any).init(wasm); + + return new sdk.BitwardenClient(...args); +}; diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 12d5b109c20..113cd736c6a 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs"; +import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap, catchError, of } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -8,7 +9,9 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; @@ -20,6 +23,7 @@ import { ToastService, } from "@bitwarden/components"; +import { flagEnabled } from "../platform/flags"; import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service"; import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; @@ -62,7 +66,28 @@ export class AppComponent implements OnInit, OnDestroy { private toastService: ToastService, private accountService: AccountService, private animationControlService: AnimationControlService, - ) {} + private logService: LogService, + private sdkService: SdkService, + ) { + if (flagEnabled("sdk")) { + // Warn if the SDK for some reason can't be initialized + this.sdkService.supported$ + .pipe( + takeUntilDestroyed(), + catchError(() => { + return of(false); + }), + ) + .subscribe((supported) => { + if (!supported) { + this.logService.debug("SDK is not supported"); + this.sdkService.failedToInitialize().catch(this.logService.error); + } else { + this.logService.debug("SDK is supported"); + } + }); + } + } async ngOnInit() { initPopupClosedListener(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 5760349f478..2c34cc3a888 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -72,6 +72,7 @@ import { PlatformUtilsService, PlatformUtilsService as PlatformUtilsServiceAbstraction, } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, @@ -80,9 +81,11 @@ import { import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; +import { flagEnabled } from "@bitwarden/common/platform/misc/flags"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { @@ -130,6 +133,7 @@ import BrowserLocalStorageService from "../../platform/services/browser-local-st import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; +import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk-client-factory"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; @@ -604,6 +608,11 @@ const safeProviders: SafeProvider[] = [ useClass: LoginEmailService, deps: [AccountService, AuthService, StateProvider], }), + safeProvider({ + provide: SdkClientFactory, + useClass: flagEnabled("sdk") ? BrowserSdkClientFactory : NoopSdkClientFactory, + deps: [], + }), ]; @NgModule({ diff --git a/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj b/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj index 6e5bc6b66c8..7642e7d1859 100644 --- a/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj +++ b/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */ = {isa = PBXBuildFile; fileRef = 03100CAE291891F4008E14EF /* encrypt-worker.js */; }; + 55BC93932CB4268A008CA4C6 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 55BC93922CB4268A008CA4C6 /* assets */; }; 55E0374D2577FA6B00979016 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E0374C2577FA6B00979016 /* AppDelegate.swift */; }; 55E037502577FA6B00979016 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 55E0374E2577FA6B00979016 /* Main.storyboard */; }; 55E037522577FA6B00979016 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E037512577FA6B00979016 /* ViewController.swift */; }; @@ -54,6 +55,7 @@ /* Begin PBXFileReference section */ 03100CAE291891F4008E14EF /* encrypt-worker.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "encrypt-worker.js"; path = "../../../build/encrypt-worker.js"; sourceTree = ""; }; 5508DD7926051B5900A85C58 /* libswiftAppKit.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftAppKit.tbd; path = usr/lib/swift/libswiftAppKit.tbd; sourceTree = SDKROOT; }; + 55BC93922CB4268A008CA4C6 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../build/assets; sourceTree = ""; }; 55E037482577FA6B00979016 /* desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktop.app; sourceTree = BUILT_PRODUCTS_DIR; }; 55E0374B2577FA6B00979016 /* desktop.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = desktop.entitlements; sourceTree = ""; }; 55E0374C2577FA6B00979016 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -152,6 +154,7 @@ 55E0376F2577FA6F00979016 /* Resources */ = { isa = PBXGroup; children = ( + 55BC93922CB4268A008CA4C6 /* assets */, 03100CAE291891F4008E14EF /* encrypt-worker.js */, 55E037702577FA6F00979016 /* popup */, 55E037712577FA6F00979016 /* background.js */, @@ -270,6 +273,7 @@ 55E0377A2577FA6F00979016 /* background.js in Resources */, 55E037792577FA6F00979016 /* popup in Resources */, 03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */, + 55BC93932CB4268A008CA4C6 /* assets in Resources */, 55E0377C2577FA6F00979016 /* notification in Resources */, 55E0377E2577FA6F00979016 /* vendor.js in Resources */, 55E0377D2577FA6F00979016 /* content in Resources */, diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 7ac5a635b18..5436e51011a 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -122,7 +122,7 @@ const moduleRules = [ loader: "@ngtools/webpack", }, { - test: /\.wasm$/, + test: /argon2(-simd)?\.wasm$/, loader: "base64-loader", type: "javascript/auto", }, @@ -316,13 +316,18 @@ const mainConfig = { }, output: { filename: "[name].js", + chunkFilename: "assets/[name].js", + webassemblyModuleFilename: "assets/[modulehash].wasm", path: path.resolve(__dirname, "build"), clean: true, }, module: { - noParse: /\.wasm$/, + noParse: /argon2(-simd)?\.wasm$/, rules: moduleRules, }, + experiments: { + asyncWebAssembly: true, + }, plugins: plugins, }; @@ -395,12 +400,15 @@ if (manifestVersion == 2) { loader: "ts-loader", }, { - test: /\.wasm$/, + test: /argon2(-simd)?\.wasm$/, loader: "base64-loader", type: "javascript/auto", }, ], - noParse: /\.wasm$/, + noParse: /argon2(-simd)?\.wasm$/, + }, + experiments: { + asyncWebAssembly: true, }, resolve: { extensions: [".ts", ".js"], diff --git a/apps/cli/config/base.json b/apps/cli/config/base.json new file mode 100644 index 00000000000..ccf867c0dcc --- /dev/null +++ b/apps/cli/config/base.json @@ -0,0 +1,5 @@ +{ + "flags": { + "sdk": true + } +} diff --git a/apps/cli/config/config.js b/apps/cli/config/config.js index 81e2d619fee..cff42ecf62e 100644 --- a/apps/cli/config/config.js +++ b/apps/cli/config/config.js @@ -1,7 +1,27 @@ function load(envName) { + const base = require("./base.json"); + const env = loadConfig(envName); + const local = loadConfig("local"); + return { - ...loadConfig(envName), - ...loadConfig("local"), + ...base, + ...env, + ...local, + dev: { + ...base.dev, + ...env.dev, + ...local.dev, + }, + flags: { + ...base.flags, + ...env.flags, + ...local.flags, + }, + devFlags: { + ...base.devFlags, + ...env.devFlags, + ...local.devFlags, + }, }; } diff --git a/apps/cli/jest.config.js b/apps/cli/jest.config.js index 8765ccc8e4e..e0a5b9ec9cc 100644 --- a/apps/cli/jest.config.js +++ b/apps/cli/jest.config.js @@ -10,7 +10,11 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: { + "@bitwarden/common/platform/services/sdk/default-sdk-client-factory": + "/../../libs/common/spec/jest-sdk-client-factory", + ...pathsToModuleNameMapper(compilerOptions?.paths || {}, { + prefix: "/", + }), + }, }; diff --git a/apps/cli/package.json b/apps/cli/package.json index 759fb14ba5a..502e186d346 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.9.1", + "version": "2024.10.0", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 280891edf36..a249a4d3f32 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -64,6 +64,7 @@ import { RegionConfig, } from "@bitwarden/common/platform/abstractions/environment.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { MessageSender } from "@bitwarden/common/platform/messaging"; @@ -86,6 +87,9 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; +import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; +import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; @@ -151,6 +155,7 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { flagEnabled } from "../platform/flags"; import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service"; import { ConsoleLogService } from "../platform/services/console-log.service"; import { I18nService } from "../platform/services/i18n.service"; @@ -249,6 +254,7 @@ export class ServiceContainer { userAutoUnlockKeyService: UserAutoUnlockKeyService; kdfConfigService: KdfConfigServiceAbstraction; taskSchedulerService: TaskSchedulerService; + sdkService: SdkService; constructor() { let p = null; @@ -522,6 +528,17 @@ export class ServiceContainer { this.globalStateProvider, ); + const sdkClientFactory = flagEnabled("sdk") + ? new DefaultSdkClientFactory() + : new NoopSdkClientFactory(); + this.sdkService = new DefaultSdkService( + sdkClientFactory, + this.environmentService, + this.platformUtilsService, + this.apiService, + customUserAgent, + ); + this.passwordStrengthService = new PasswordStrengthService(); this.passwordGenerationService = legacyPasswordGenerationServiceFactory( @@ -830,5 +847,19 @@ export class ServiceContainer { } this.inited = true; + + if (flagEnabled("sdk")) { + // Warn if the SDK for some reason can't be initialized + let supported = false; + try { + supported = await firstValueFrom(this.sdkService.supported$); + } catch (e) { + // Do nothing. + } + + if (!supported) { + this.sdkService.failedToInitialize().catch(this.logService.error); + } + } } } diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index eedf24179d0..4cb450f9c69 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -3,7 +3,7 @@ "pretty": true, "moduleResolution": "node", "target": "ES2016", - "module": "es6", + "module": "ES2020", "noImplicitAny": true, "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, diff --git a/apps/cli/webpack.config.js b/apps/cli/webpack.config.js index 2b9c53bac64..d5f66af73ec 100644 --- a/apps/cli/webpack.config.js +++ b/apps/cli/webpack.config.js @@ -37,8 +37,10 @@ const plugins = [ contextRegExp: /node-fetch/, }), new webpack.EnvironmentPlugin({ + ENV: ENV, BWCLI_ENV: ENV, FLAGS: envConfig.flags, + DEV_FLAGS: envConfig.devFlags, }), new webpack.IgnorePlugin({ resourceRegExp: /canvas/, @@ -79,6 +81,9 @@ const webpackConfig = { allowlist: [/@bitwarden/], }), ], + experiments: { + asyncWebAssembly: true, + }, }; module.exports = webpackConfig; diff --git a/apps/desktop/config/base.json b/apps/desktop/config/base.json index 3c93018e65f..5d045326d45 100644 --- a/apps/desktop/config/base.json +++ b/apps/desktop/config/base.json @@ -1,4 +1,6 @@ { - "devFlags": {}, - "flags": {} + "flags": { + "sdk": true + }, + "devFlags": {} } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index fbf598327f4..1014412c0d5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.9.2", + "version": "2024.10.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 9678f657234..61da12998d1 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -8,8 +8,9 @@ import { ViewChild, ViewContainerRef, } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; -import { filter, firstValueFrom, map, Subject, takeUntil, timeout } from "rxjs"; +import { catchError, filter, firstValueFrom, map, of, Subject, takeUntil, timeout } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -21,7 +22,6 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; @@ -38,6 +38,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; @@ -56,6 +57,7 @@ import { BiometricStateService } from "@bitwarden/key-management"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginApprovalComponent } from "../auth/login/login-approval.component"; import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater"; +import { flagEnabled } from "../platform/flags"; import { PremiumComponent } from "../vault/app/accounts/premium.component"; import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component"; @@ -150,9 +152,28 @@ export class AppComponent implements OnInit, OnDestroy { private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, - private providerService: ProviderService, private accountService: AccountService, - ) {} + private sdkService: SdkService, + ) { + if (flagEnabled("sdk")) { + // Warn if the SDK for some reason can't be initialized + this.sdkService.supported$ + .pipe( + takeUntilDestroyed(), + catchError(() => { + return of(false); + }), + ) + .subscribe((supported) => { + if (!supported) { + this.logService.debug("SDK is not supported"); + this.sdkService.failedToInitialize().catch(this.logService.error); + } else { + this.logService.debug("SDK is supported"); + } + }); + } + } ngOnInit() { this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 7860cc1ea9b..e6ca38e3384 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -65,6 +65,7 @@ import { } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; @@ -73,6 +74,8 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; +import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; +import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage @@ -87,6 +90,7 @@ import { BiometricStateService, BiometricsService } from "@bitwarden/key-managem import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service"; +import { flagEnabled } from "../../platform/flags"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { ElectronCryptoService } from "../../platform/services/electron-crypto.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; @@ -332,6 +336,11 @@ const safeProviders: SafeProvider[] = [ useClass: LoginEmailService, deps: [AccountService, AuthService, StateProvider], }), + safeProvider({ + provide: SdkClientFactory, + useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory, + deps: [], + }), ]; @NgModule({ diff --git a/apps/desktop/src/index.html b/apps/desktop/src/index.html index 6bac674bb5e..37eb64adf35 100644 --- a/apps/desktop/src/index.html +++ b/apps/desktop/src/index.html @@ -4,7 +4,7 @@ diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index ec57ecdf7bb..9c9f1ae6a9b 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -331,7 +331,7 @@ export class NativeMessagingMain { const ext = process.platform === "win32" ? ".exe" : ""; if (isDev()) { - return path.join( + const devPath = path.join( this.appPath, "..", "desktop_native", @@ -339,6 +339,12 @@ export class NativeMessagingMain { "debug", `desktop_proxy${ext}`, ); + + // isDev() returns true when using a production build with ELECTRON_IS_DEV=1, + // so we need to fall back to the prod binary if the dev binary doesn't exist. + if (existsSync(devPath)) { + return devPath; + } } return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`); diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index acd5f97a3f4..602f35872e1 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,26 +1,18 @@ { "name": "@bitwarden/desktop", - "version": "2024.9.2", + "version": "2024.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.9.2", + "version": "2024.10.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi", "argon2": "0.40.1" } }, - "../desktop_native/napi": { - "name": "@bitwarden/desktop-napi", - "version": "0.1.0", - "license": "GPL-3.0", - "devDependencies": { - "@napi-rs/cli": "2.16.2" - } - }, "../desktop_native/napi": { "version": "0.1.0", "license": "GPL-3.0", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 29b3f8ed7d1..c98d31f10a2 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.9.2", + "version": "2024.10.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/webpack.renderer.js b/apps/desktop/webpack.renderer.js index dc3cdf1fef5..ac990689ae0 100644 --- a/apps/desktop/webpack.renderer.js +++ b/apps/desktop/webpack.renderer.js @@ -42,7 +42,7 @@ const common = { type: "asset/resource", }, { - test: /\.wasm$/, + test: /argon2(-simd)?\.wasm$/, loader: "base64-loader", type: "javascript/auto", }, @@ -143,11 +143,15 @@ const renderer = { parser: { system: true }, }, { - test: /\.wasm$/, + test: /argon2(-simd)?\.wasm$/, loader: "base64-loader", type: "javascript/auto", }, ], + noParse: /argon2(-simd)?\.wasm$/, + }, + experiments: { + asyncWebAssembly: true, }, plugins: [ new AngularWebpackPlugin({ diff --git a/apps/web/config/base.json b/apps/web/config/base.json index 8eb8a311335..cfaf604fb02 100644 --- a/apps/web/config/base.json +++ b/apps/web/config/base.json @@ -11,6 +11,8 @@ "allowedHosts": "auto" }, "flags": { - "showPasswordless": false - } + "showPasswordless": false, + "sdk": true + }, + "devFlags": {} } diff --git a/apps/web/jest.config.js b/apps/web/jest.config.js index f121823adee..9b5d6fdc766 100644 --- a/apps/web/jest.config.js +++ b/apps/web/jest.config.js @@ -9,11 +9,19 @@ module.exports = { ...sharedConfig, preset: "jest-preset-angular", setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper( - // lets us use @bitwarden/common/spec in web tests - { "@bitwarden/common/spec": ["../../libs/common/spec"], ...(compilerOptions?.paths ?? {}) }, - { - prefix: "/", - }, - ), + moduleNameMapper: { + // Replace ESM SDK with Node compatible SDK + "@bitwarden/common/platform/services/sdk/default-sdk-client-factory": + "/../../libs/common/spec/jest-sdk-client-factory", + ...pathsToModuleNameMapper( + { + // lets us use @bitwarden/common/spec in web tests + "@bitwarden/common/spec": ["../../libs/common/spec"], + ...(compilerOptions?.paths ?? {}), + }, + { + prefix: "/", + }, + ), + }, }; diff --git a/apps/web/package.json b/apps/web/package.json index bc7a96e239f..dd737fb5b43 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.10.0", + "version": "2024.10.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 578bc9111cf..7299c8ece22 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -1,8 +1,9 @@ import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs"; +import { Subject, filter, firstValueFrom, map, takeUntil, timeout, catchError, of } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -19,7 +20,9 @@ import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broa import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { SyncService } from "@bitwarden/common/platform/sync"; @@ -30,6 +33,8 @@ import { DialogService, ToastOptions, ToastService } from "@bitwarden/components import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BiometricStateService } from "@bitwarden/key-management"; +import { flagEnabled } from "../utils/flags"; + import { PolicyListService } from "./admin-console/core/policy-list.service"; import { DisableSendPolicy, @@ -85,7 +90,28 @@ export class AppComponent implements OnDestroy, OnInit { private stateEventRunnerService: StateEventRunnerService, private organizationService: InternalOrganizationServiceAbstraction, private accountService: AccountService, - ) {} + private logService: LogService, + private sdkService: SdkService, + ) { + if (flagEnabled("sdk")) { + // Warn if the SDK for some reason can't be initialized + this.sdkService.supported$ + .pipe( + takeUntilDestroyed(), + catchError(() => { + return of(false); + }), + ) + .subscribe((supported) => { + if (!supported) { + this.logService.debug("SDK is not supported"); + this.sdkService.failedToInitialize().catch(this.logService.error); + } else { + this.logService.debug("SDK is supported"); + } + }); + } + } ngOnInit() { this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index b5d35c39939..99b70bb5589 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -58,6 +58,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service"; @@ -65,6 +66,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; /* eslint-disable import/no-restricted-paths -- Implementation for memory storage */ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; @@ -80,6 +82,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BiometricsService } from "@bitwarden/key-management"; +import { flagEnabled } from "../../utils/flags"; import { PolicyListService } from "../admin-console/core/policy-list.service"; import { WebSetPasswordJitService, @@ -93,6 +96,7 @@ import { I18nService } from "../core/i18n.service"; import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; +import { WebSdkClientFactory } from "../platform/web-sdk-client-factory"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; import { EventService } from "./event.service"; @@ -270,6 +274,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultCollectionAdminService, deps: [ApiService, CryptoServiceAbstraction, EncryptService, CollectionService], }), + safeProvider({ + provide: SdkClientFactory, + useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory, + deps: [], + }), ]; @NgModule({ diff --git a/apps/web/src/app/platform/web-sdk-client-factory.ts b/apps/web/src/app/platform/web-sdk-client-factory.ts new file mode 100644 index 00000000000..2ebb2bcc10f --- /dev/null +++ b/apps/web/src/app/platform/web-sdk-client-factory.ts @@ -0,0 +1,42 @@ +import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import * as sdk from "@bitwarden/sdk-internal"; + +/** + * SDK client factory with a js fallback for when WASM is not supported. + */ +export class WebSdkClientFactory implements SdkClientFactory { + async createSdkClient( + ...args: ConstructorParameters + ): Promise { + const module = await load(); + + (sdk as any).init(module); + + return Promise.resolve(new sdk.BitwardenClient(...args)); + } +} + +// https://stackoverflow.com/a/47880734 +const supported = (() => { + try { + if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { + const module = new WebAssembly.Module( + Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00), + ); + if (module instanceof WebAssembly.Module) { + return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } + } catch (e) { + // ignore + } + return false; +})(); + +async function load() { + if (supported) { + return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); + } else { + return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"); + } +} diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html index 521665496ad..740264713ca 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html @@ -65,7 +65,7 @@ > {{ "cancel" | i18n }} -
+
diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index d3d0703605f..c1878e2dcb7 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -179,6 +179,15 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return this.cipher?.edit ?? false; } + protected get showDelete() { + // Don't show the delete button when cloning a cipher + if (this.params.mode == "form" && this.formConfig.mode === "clone") { + return false; + } + // Never show the delete button for new ciphers + return this.cipher != null; + } + protected get showCipherView() { return this.cipher != undefined && (this.params.mode === "view" || this.loadingForm); } @@ -332,8 +341,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { }; cancel = async () => { - // We're in View mode, or we don't have a cipher, close the dialog. - if (this.params.mode === "view" || this.cipher == null) { + // We're in View mode, we don't have a cipher, or we were cloning, close the dialog. + if (this.params.mode === "view" || this.cipher == null || this.formConfig.mode === "clone") { this.dialogRef.close(this._cipherModified ? VaultItemDialogResult.Saved : undefined); return; } diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts new file mode 100644 index 00000000000..f0624e6b2f2 --- /dev/null +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -0,0 +1,119 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { CollectionAdminService } from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; + +import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; + +import { AdminConsoleCipherFormConfigService } from "./admin-console-cipher-form-config.service"; + +describe("AdminConsoleCipherFormConfigService", () => { + let adminConsoleConfigService: AdminConsoleCipherFormConfigService; + + const cipherId = "333-444-555" as CipherId; + const testOrg = { id: "333-44-55", name: "Test Org", canEditAllCiphers: false }; + const organization$ = new BehaviorSubject(testOrg as Organization); + const getCipherAdmin = jest.fn().mockResolvedValue(null); + const getCipher = jest.fn().mockResolvedValue(null); + + beforeEach(async () => { + getCipherAdmin.mockClear(); + getCipher.mockClear(); + getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher - (non-admin)" }); + getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); + + await TestBed.configureTestingModule({ + providers: [ + AdminConsoleCipherFormConfigService, + { provide: OrganizationService, useValue: { get$: () => organization$ } }, + { provide: CipherService, useValue: { get: getCipher } }, + { provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } }, + { + provide: RoutedVaultFilterService, + useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) }, + }, + { provide: ApiService, useValue: { getCipherAdmin } }, + ], + }); + }); + + describe("buildConfig", () => { + it("sets individual attributes", async () => { + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); + + const { folders, hideIndividualVaultFields } = await adminConsoleConfigService.buildConfig( + "add", + cipherId, + ); + + expect(folders).toEqual([]); + expect(hideIndividualVaultFields).toBe(true); + }); + + it("sets mode based on passed mode", async () => { + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); + + const { mode } = await adminConsoleConfigService.buildConfig("edit", cipherId); + + expect(mode).toBe("edit"); + }); + + it("sets admin flag based on `canEditAllCiphers`", async () => { + // Disable edit all ciphers on org + testOrg.canEditAllCiphers = false; + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); + + let result = await adminConsoleConfigService.buildConfig("add", cipherId); + + expect(result.admin).toBe(false); + + // Enable edit all ciphers on org + testOrg.canEditAllCiphers = true; + result = await adminConsoleConfigService.buildConfig("add", cipherId); + + expect(result.admin).toBe(true); + }); + + it("sets `allowPersonalOwnership` to false", async () => { + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); + + const result = await adminConsoleConfigService.buildConfig("clone", cipherId); + + expect(result.allowPersonalOwnership).toBe(false); + }); + + describe("getCipher", () => { + it("retrieves the cipher from the cipher service", async () => { + testOrg.canEditAllCiphers = false; + + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); + + const result = await adminConsoleConfigService.buildConfig("clone", cipherId); + + expect(getCipher).toHaveBeenCalledWith(cipherId); + expect(result.originalCipher.name).toBe("Test Cipher - (non-admin)"); + + // Admin service not needed when cipher service can return the cipher + expect(getCipherAdmin).not.toHaveBeenCalled(); + }); + + it("retrieves the cipher from the admin service", async () => { + getCipher.mockResolvedValueOnce(null); + getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" }); + + adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); + + await adminConsoleConfigService.buildConfig("add", cipherId); + + expect(getCipherAdmin).toHaveBeenCalledWith(cipherId); + + expect(getCipher).toHaveBeenCalledWith(cipherId); + }); + }); + }); +}); diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts new file mode 100644 index 00000000000..fa5cbedfca8 --- /dev/null +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -0,0 +1,99 @@ +import { inject, Injectable } from "@angular/core"; +import { combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs"; + +import { CollectionAdminService } from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; + +import { + CipherFormConfig, + CipherFormConfigService, + CipherFormMode, +} from "../../../../../../../libs/vault/src/cipher-form/abstractions/cipher-form-config.service"; +import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; + +/** Admin Console implementation of the `CipherFormConfigService`. */ +@Injectable() +export class AdminConsoleCipherFormConfigService implements CipherFormConfigService { + private organizationService: OrganizationService = inject(OrganizationService); + private cipherService: CipherService = inject(CipherService); + private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService); + private collectionAdminService: CollectionAdminService = inject(CollectionAdminService); + private apiService: ApiService = inject(ApiService); + + private organizationId$ = this.routedVaultFilterService.filter$.pipe( + map((filter) => filter.organizationId), + filter((filter) => filter !== undefined), + ); + + private organization$ = this.organizationId$.pipe( + switchMap((organizationId) => this.organizationService.get$(organizationId)), + ); + + private editableCollections$ = this.organization$.pipe( + switchMap(async (org) => { + const collections = await this.collectionAdminService.getAll(org.id); + // Users that can edit all ciphers can implicitly add to / edit within any collection + if (org.canEditAllCiphers) { + return collections; + } + // The user is only allowed to add/edit items to assigned collections that are not readonly + return collections.filter((c) => c.assigned && !c.readOnly); + }), + ); + + async buildConfig( + mode: CipherFormMode, + cipherId?: CipherId, + cipherType?: CipherType, + ): Promise { + const [organization, allCollections] = await firstValueFrom( + combineLatest([this.organization$, this.editableCollections$]), + ); + + const cipher = await this.getCipher(organization, cipherId); + + const collections = allCollections.filter( + (c) => c.organizationId === organization.id && c.assigned && !c.readOnly, + ); + + return { + mode, + cipherType: cipher?.type ?? cipherType ?? CipherType.Login, + admin: organization.canEditAllCiphers ?? false, + allowPersonalOwnership: false, + originalCipher: cipher, + collections, + organizations: [organization], // only a single org is in context at a time + folders: [], // folders not applicable in the admin console + hideIndividualVaultFields: true, + }; + } + + private async getCipher(organization: Organization, id?: CipherId): Promise { + if (id == null) { + return Promise.resolve(null); + } + + // Check to see if the user has direct access to the cipher + const cipherFromCipherService = await this.cipherService.get(id); + + // If the organization doesn't allow admin/owners to edit all ciphers return the cipher + if (!organization.canEditAllCiphers && cipherFromCipherService != null) { + return cipherFromCipherService; + } + + // Retrieve the cipher through the means of an admin + const cipherResponse = await this.apiService.getCipherAdmin(id); + cipherResponse.edit = true; + + const cipherData = new CipherData(cipherResponse); + return new Cipher(cipherData); + } +} diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index a52530dde1a..9c8ff3f2ff5 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -1,3 +1,4 @@ +import { DialogRef } from "@angular/cdk/dialog"; import { ChangeDetectorRef, Component, @@ -42,16 +43,17 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -62,7 +64,12 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DialogService, Icons, NoItemsModule, ToastService } from "@bitwarden/components"; -import { CollectionAssignmentResult, PasswordRepromptService } from "@bitwarden/vault"; +import { + CipherFormConfig, + CipherFormConfigService, + CollectionAssignmentResult, + PasswordRepromptService, +} from "@bitwarden/vault"; import { GroupService, GroupView } from "../../admin-console/organizations/core"; import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component"; @@ -75,6 +82,11 @@ import { CollectionDialogTabType, openCollectionDialog, } from "../components/collection-dialog"; +import { + VaultItemDialogComponent, + VaultItemDialogMode, + VaultItemDialogResult, +} from "../components/vault-item-dialog/vault-item-dialog.component"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; import { @@ -89,12 +101,6 @@ import { All, RoutedVaultFilterModel, } from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; -import { - openViewCipherDialog, - ViewCipherDialogCloseResult, - ViewCipherDialogResult, - ViewComponent, -} from "../individual-vault/view.component"; import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.component"; import { getNestedCollectionTree } from "../utils/collection-utils"; @@ -106,8 +112,8 @@ import { } from "./bulk-collections-dialog"; import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component"; import { openOrgVaultCollectionsDialog } from "./collections.component"; +import { AdminConsoleCipherFormConfigService } from "./services/admin-console-cipher-form-config.service"; import { VaultFilterModule } from "./vault-filter/vault-filter.module"; - const BroadcasterSubscriptionId = "OrgVaultComponent"; const SearchTextDebounceInterval = 200; @@ -127,9 +133,12 @@ enum AddAccessStatusType { VaultItemsModule, SharedModule, NoItemsModule, - ViewComponent, ], - providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService], + providers: [ + RoutedVaultFilterService, + RoutedVaultFilterBridgeService, + { provide: CipherFormConfigService, useClass: AdminConsoleCipherFormConfigService }, + ], }) export class VaultComponent implements OnInit, OnDestroy { protected Unassigned = Unassigned; @@ -174,6 +183,8 @@ export class VaultComponent implements OnInit, OnDestroy { private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); protected addAccessStatus$ = new BehaviorSubject(0); + private extensionRefreshEnabled: boolean; + private vaultItemDialogRef?: DialogRef | undefined; constructor( private route: ActivatedRoute, @@ -203,10 +214,15 @@ export class VaultComponent implements OnInit, OnDestroy { private apiService: ApiService, private collectionService: CollectionService, private toastService: ToastService, - private accountService: AccountService, + private configService: ConfigService, + private cipherFormConfigService: CipherFormConfigService, ) {} async ngOnInit() { + this.extensionRefreshEnabled = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); + this.trashCleanupWarning = this.i18nService.t( this.platformUtilsService.isSelfHost() ? "trashCleanupWarningSelfHosted" @@ -466,22 +482,27 @@ export class VaultComponent implements OnInit, OnDestroy { firstSetup$ .pipe( switchMap(() => this.route.queryParams), + // Only process the queryParams if the dialog is not open (only when extension refresh is enabled) + filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled), withLatestFrom(allCipherMap$, allCollections$, organization$), - switchMap(async ([qParams, allCiphersMap, allCollections]) => { + switchMap(async ([qParams, allCiphersMap]) => { const cipherId = getCipherIdFromParams(qParams); if (!cipherId) { return; } const cipher = allCiphersMap[cipherId]; - const cipherCollections = allCollections.filter((c) => - cipher.collectionIds.includes(c.id), - ); if (cipher) { - if (qParams.action === "view") { - await this.viewCipher(cipher, cipherCollections); + let action = qParams.action; + // Default to "view" if extension refresh is enabled + if (action == null && this.extensionRefreshEnabled) { + action = "view"; + } + + if (action === "view") { + await this.viewCipherById(cipher); } else { - await this.editCipherId(cipherId); + await this.editCipherId(cipher, false); } } else { this.toastService.showToast({ @@ -730,12 +751,16 @@ export class VaultComponent implements OnInit, OnDestroy { } async addCipher(cipherType?: CipherType) { + if (this.extensionRefreshEnabled) { + return this.addCipherV2(cipherType); + } + let collections: CollectionView[] = []; // Admins limited to only adding items to collections they have access to. collections = await firstValueFrom(this.editableCollections$); - await this.editCipher(null, (comp) => { + await this.editCipher(null, false, (comp) => { comp.type = cipherType || this.activeFilter.cipherType; comp.collections = collections; if (this.activeFilter.collectionId) { @@ -744,20 +769,46 @@ export class VaultComponent implements OnInit, OnDestroy { }); } + /** Opens the Add/Edit Dialog. Only to be used when the BrowserExtension feature flag is active */ + async addCipherV2(cipherType?: CipherType) { + const cipherFormConfig = await this.cipherFormConfigService.buildConfig( + "add", + null, + cipherType, + ); + + const collectionId: CollectionId | undefined = this.activeFilter.collectionId as CollectionId; + + cipherFormConfig.initialValues = { + organizationId: this.organization.id as OrganizationId, + collectionIds: collectionId ? [collectionId] : [], + }; + + await this.openVaultItemDialog("form", cipherFormConfig); + } + + /** + * Edit the given cipher + * @param cipherView - The cipher to be edited + * @param cloneCipher - `true` when the cipher should be cloned. + * Used in place of the `additionalComponentParameters`, as + * the `editCipherIdV2` method has a differing implementation. + * @param defaultComponentParameters - A method that takes in an instance of + * the `AddEditComponent` to edit methods directly. + */ async editCipher( cipher: CipherView, + cloneCipher: boolean, additionalComponentParameters?: (comp: AddEditComponent) => void, ) { - return this.editCipherId(cipher?.id, additionalComponentParameters); + return this.editCipherId(cipher, cloneCipher, additionalComponentParameters); } async editCipherId( - cipherId: string, + cipher: CipherView, + cloneCipher: boolean, additionalComponentParameters?: (comp: AddEditComponent) => void, ) { - const cipher = await this.cipherService.get(cipherId); - // if cipher exists (cipher is null when new) and MP reprompt - // is on for this cipher, then show password reprompt if ( cipher && cipher.reprompt !== 0 && @@ -768,10 +819,15 @@ export class VaultComponent implements OnInit, OnDestroy { return; } + if (this.extensionRefreshEnabled) { + await this.editCipherIdV2(cipher, cloneCipher); + return; + } + const defaultComponentParameters = (comp: AddEditComponent) => { comp.organization = this.organization; comp.organizationId = this.organization.id; - comp.cipherId = cipherId; + comp.cipherId = cipher.id; comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); this.refresh(); @@ -807,46 +863,70 @@ export class VaultComponent implements OnInit, OnDestroy { } /** - * Takes a cipher and its assigned collections to opens dialog where it can be viewed. - * @param cipher - the cipher to view - * @param collections - the collections the cipher is assigned to + * Edit a cipher using the new AddEditCipherDialogV2 component. + * Only to be used behind the ExtensionRefresh feature flag. */ - async viewCipher(cipher: CipherView, collections: CollectionView[] = []) { + private async editCipherIdV2(cipher: CipherView, cloneCipher: boolean) { + const cipherFormConfig = await this.cipherFormConfigService.buildConfig( + cloneCipher ? "clone" : "edit", + cipher.id as CipherId, + ); + + await this.openVaultItemDialog("form", cipherFormConfig, cipher); + } + + /** Opens the view dialog for the given cipher unless password reprompt fails */ + async viewCipherById(cipher: CipherView) { if (!cipher) { - this.go({ cipherId: null, itemId: null }); return; } - if (cipher.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) { - // didn't pass password prompt, so don't open the dialog - this.go({ cipherId: null, itemId: null }); + if ( + cipher && + cipher.reprompt !== 0 && + !(await this.passwordRepromptService.showPasswordPrompt()) + ) { + // Didn't pass password prompt, so don't open add / edit modal. + await this.go({ cipherId: null, itemId: null, action: null }); return; } - const dialogRef = openViewCipherDialog(this.dialogService, { - data: { - cipher: cipher, - collections: collections, - disableEdit: !cipher.edit && !this.organization.canEditAllCiphers, - }, + const cipherFormConfig = await this.cipherFormConfigService.buildConfig( + "edit", + cipher.id as CipherId, + cipher.type, + ); + + await this.openVaultItemDialog("view", cipherFormConfig, cipher); + } + + /** + * Open the combined view / edit dialog for a cipher. + */ + async openVaultItemDialog( + mode: VaultItemDialogMode, + formConfig: CipherFormConfig, + cipher?: CipherView, + ) { + const disableForm = cipher ? !cipher.edit && !this.organization.canEditAllCiphers : false; + // If the form is disabled, force the mode into `view` + const dialogMode = disableForm ? "view" : mode; + this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, { + mode: dialogMode, + formConfig, + disableForm, }); - // Wait for the dialog to close. - const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed); - - // If the dialog was closed by clicking the edit button, navigate to open the edit dialog. - if (result?.action === ViewCipherDialogResult.Edited) { - this.go({ itemId: cipher.id, action: "edit" }); - return; - } + const result = await lastValueFrom(this.vaultItemDialogRef.closed); + this.vaultItemDialogRef = undefined; // If the dialog was closed by deleting the cipher, refresh the vault. - if (result?.action === ViewCipherDialogResult.Deleted) { + if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) { this.refresh(); } - // Clear the query params when the view dialog closes - this.go({ cipherId: null, itemId: null, action: null }); + // Clear the query params when the dialog closes + await this.go({ cipherId: null, itemId: null, action: null }); } async cloneCipher(cipher: CipherView) { @@ -867,7 +947,7 @@ export class VaultComponent implements OnInit, OnDestroy { // Admins limited to only adding items to collections they have access to. collections = await firstValueFrom(this.editableCollections$); - await this.editCipher(cipher, (comp) => { + await this.editCipher(cipher, true, (comp) => { comp.cloneMode = true; comp.collections = collections; comp.collectionIds = cipher.collectionIds; diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index fee60264ea4..df325015aad 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -78,7 +78,7 @@ const moduleRules = [ loader: "@ngtools/webpack", }, { - test: /\.wasm$/, + test: /argon2(-simd)?\.wasm$/, loader: "base64-loader", type: "javascript/auto", }, @@ -324,6 +324,7 @@ const webpackConfig = { mode: NODE_ENV, devtool: "source-map", devServer: devServer, + target: "web", entry: { "app/polyfills": "./src/polyfills.ts", "app/main": "./src/main.ts", @@ -383,9 +384,12 @@ const webpackConfig = { clean: true, }, module: { - noParse: /\.wasm$/, + noParse: /argon2(-simd)?\.wasm$/, rules: moduleRules, }, + experiments: { + asyncWebAssembly: true, + }, plugins: plugins, }; diff --git a/bitwarden_license/bit-cli/jest.config.js b/bitwarden_license/bit-cli/jest.config.js index 92be98cc561..30c9784c326 100644 --- a/bitwarden_license/bit-cli/jest.config.js +++ b/bitwarden_license/bit-cli/jest.config.js @@ -10,7 +10,11 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", setupFilesAfterEnv: ["/../../apps/cli/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: { + "@bitwarden/common/platform/services/sdk/default-sdk-client-factory": + "/../../libs/common/spec/jest-sdk-client-factory", + ...pathsToModuleNameMapper(compilerOptions?.paths || {}, { + prefix: "/", + }), + }, }; diff --git a/bitwarden_license/bit-cli/tsconfig.json b/bitwarden_license/bit-cli/tsconfig.json index bb9986e6c9d..9440a03375a 100644 --- a/bitwarden_license/bit-cli/tsconfig.json +++ b/bitwarden_license/bit-cli/tsconfig.json @@ -3,7 +3,7 @@ "pretty": true, "moduleResolution": "node", "target": "ES2016", - "module": "es6", + "module": "ES2020", "noImplicitAny": true, "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 537eb577a4a..97033f3a5bb 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -153,6 +153,8 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -181,6 +183,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; +import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; @@ -1337,6 +1340,16 @@ const safeProviders: SafeProvider[] = [ SsoLoginServiceAbstraction, ], }), + safeProvider({ + provide: SdkService, + useClass: DefaultSdkService, + deps: [ + SdkClientFactory, + EnvironmentService, + PlatformUtilsServiceAbstraction, + ApiServiceAbstraction, + ], + }), ]; @NgModule({ diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 960a226b1cf..dc782e6e3a9 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -605,6 +605,10 @@ export class AddEditComponent implements OnInit, OnDestroy { this.collections = this.writeableCollections?.filter( (c) => c.organizationId === this.cipher.organizationId, ); + // If there's only one collection, check it by default + if (this.collections.length === 1) { + (this.collections[0] as any).checked = true; + } const org = await this.organizationService.get(this.cipher.organizationId); if (org != null) { this.cipher.organizationUseTotp = org.useTotp; diff --git a/libs/common/spec/jest-sdk-client-factory.ts b/libs/common/spec/jest-sdk-client-factory.ts new file mode 100644 index 00000000000..ff120ccd787 --- /dev/null +++ b/libs/common/spec/jest-sdk-client-factory.ts @@ -0,0 +1,9 @@ +import { ClientSettings, LogLevel, BitwardenClient } from "@bitwarden/sdk-internal"; + +import { SdkClientFactory } from "../src/platform/abstractions/sdk/sdk-client-factory"; + +export class DefaultSdkClientFactory implements SdkClientFactory { + createSdkClient(settings?: ClientSettings, log_level?: LogLevel): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts new file mode 100644 index 00000000000..d684561dacd --- /dev/null +++ b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts @@ -0,0 +1,10 @@ +import type { BitwardenClient } from "@bitwarden/sdk-internal"; + +/** + * Factory for creating SDK clients. + */ +export abstract class SdkClientFactory { + abstract createSdkClient( + ...args: ConstructorParameters + ): Promise; +} diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts new file mode 100644 index 00000000000..360f2e91a76 --- /dev/null +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -0,0 +1,10 @@ +import { Observable } from "rxjs"; + +import { BitwardenClient } from "@bitwarden/sdk-internal"; + +export abstract class SdkService { + client$: Observable; + supported$: Observable; + + abstract failedToInitialize(): Promise; +} diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index 3a305676812..b3269c8f4e8 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -2,6 +2,7 @@ // eslint-disable-next-line @typescript-eslint/ban-types export type SharedFlags = { showPasswordless?: boolean; + sdk?: boolean; }; // required to avoid linting errors when there are no flags @@ -28,7 +29,7 @@ function getFlags(envFlags: string | T): T { * @returns The value of the flag */ export function flagEnabled(flag: keyof Flags): boolean { - const flags = getFlags(process.env.FLAGS); + const flags = getFlags(process.env.FLAGS) ?? ({} as Flags); return flags[flag] == null || !!flags[flag]; } diff --git a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts new file mode 100644 index 00000000000..8e99af2efed --- /dev/null +++ b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts @@ -0,0 +1,19 @@ +import * as sdk from "@bitwarden/sdk-internal"; +import * as module from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"; + +import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; + +/** + * Directly imports the Bitwarden SDK and initializes it. + * + * **Warning**: This requires WASM support and will fail if the environment does not support it. + */ +export class DefaultSdkClientFactory implements SdkClientFactory { + async createSdkClient( + ...args: ConstructorParameters + ): Promise { + (sdk as any).init(module); + + return Promise.resolve(new sdk.BitwardenClient(...args)); + } +} diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts new file mode 100644 index 00000000000..d4a9cfeb7ed --- /dev/null +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -0,0 +1,111 @@ +import { concatMap, firstValueFrom, shareReplay } from "rxjs"; + +import { LogLevel, DeviceType as SdkDeviceType } from "@bitwarden/sdk-internal"; + +import { ApiService } from "../../../abstractions/api.service"; +import { DeviceType } from "../../../enums/device-type.enum"; +import { EnvironmentService } from "../../abstractions/environment.service"; +import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; +import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; +import { SdkService } from "../../abstractions/sdk/sdk.service"; + +export class DefaultSdkService implements SdkService { + client$ = this.environmentService.environment$.pipe( + concatMap(async (env) => { + const settings = { + apiUrl: env.getApiUrl(), + identityUrl: env.getIdentityUrl(), + deviceType: this.toDevice(this.platformUtilsService.getDevice()), + userAgent: this.userAgent ?? navigator.userAgent, + }; + + return await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + supported$ = this.client$.pipe( + concatMap(async (client) => { + return client.echo("bitwarden wasm!") === "bitwarden wasm!"; + }), + ); + + constructor( + private sdkClientFactory: SdkClientFactory, + private environmentService: EnvironmentService, + private platformUtilsService: PlatformUtilsService, + private apiService: ApiService, // Yes we shouldn't import ApiService, but it's temporary + private userAgent: string = null, + ) {} + + async failedToInitialize(): Promise { + // Only log on cloud instances + if ( + this.platformUtilsService.isDev() || + !(await firstValueFrom(this.environmentService.environment$)).isCloud + ) { + return; + } + + return this.apiService.send("POST", "/wasm-debug", null, false, false, null, (headers) => { + headers.append("SDK-Version", "1.0.0"); + }); + } + + private toDevice(device: DeviceType): SdkDeviceType { + switch (device) { + case DeviceType.Android: + return "Android"; + case DeviceType.iOS: + return "iOS"; + case DeviceType.ChromeExtension: + return "ChromeExtension"; + case DeviceType.FirefoxExtension: + return "FirefoxExtension"; + case DeviceType.OperaExtension: + return "OperaExtension"; + case DeviceType.EdgeExtension: + return "EdgeExtension"; + case DeviceType.WindowsDesktop: + return "WindowsDesktop"; + case DeviceType.MacOsDesktop: + return "MacOsDesktop"; + case DeviceType.LinuxDesktop: + return "LinuxDesktop"; + case DeviceType.ChromeBrowser: + return "ChromeBrowser"; + case DeviceType.FirefoxBrowser: + return "FirefoxBrowser"; + case DeviceType.OperaBrowser: + return "OperaBrowser"; + case DeviceType.EdgeBrowser: + return "EdgeBrowser"; + case DeviceType.IEBrowser: + return "IEBrowser"; + case DeviceType.UnknownBrowser: + return "UnknownBrowser"; + case DeviceType.AndroidAmazon: + return "AndroidAmazon"; + case DeviceType.UWP: + return "UWP"; + case DeviceType.SafariBrowser: + return "SafariBrowser"; + case DeviceType.VivaldiBrowser: + return "VivaldiBrowser"; + case DeviceType.VivaldiExtension: + return "VivaldiExtension"; + case DeviceType.SafariExtension: + return "SafariExtension"; + case DeviceType.Server: + return "Server"; + case DeviceType.WindowsCLI: + return "WindowsCLI"; + case DeviceType.MacOsCLI: + return "MacOsCLI"; + case DeviceType.LinuxCLI: + return "LinuxCLI"; + default: + return "SDK"; + } + } +} diff --git a/libs/common/src/platform/services/sdk/noop-sdk-client-factory.ts b/libs/common/src/platform/services/sdk/noop-sdk-client-factory.ts new file mode 100644 index 00000000000..d7eab7e8dc9 --- /dev/null +++ b/libs/common/src/platform/services/sdk/noop-sdk-client-factory.ts @@ -0,0 +1,16 @@ +import type { BitwardenClient } from "@bitwarden/sdk-internal"; + +import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; + +/** + * Noop SDK client factory. + * + * Used during SDK rollout to prevent bundling the SDK with some applications. + */ +export class NoopSdkClientFactory implements SdkClientFactory { + createSdkClient( + ...args: ConstructorParameters + ): Promise { + return Promise.reject(new Error("SDK not available")); + } +} diff --git a/libs/common/src/vault/abstractions/collection.service.ts b/libs/common/src/vault/abstractions/collection.service.ts index 81ae76729a2..1f3e95a019f 100644 --- a/libs/common/src/vault/abstractions/collection.service.ts +++ b/libs/common/src/vault/abstractions/collection.service.ts @@ -8,6 +8,7 @@ import { TreeNode } from "../models/domain/tree-node"; import { CollectionView } from "../models/view/collection.view"; export abstract class CollectionService { + encryptedCollections$: Observable; decryptedCollections$: Observable; clearActiveUserCache: () => Promise; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index a06ca4d793d..77e696b5cdd 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -673,6 +673,8 @@ export class CipherService implements CipherServiceAbstraction { if (orgAdmin && cipher.organizationId != null) { const request = new CipherCreateRequest(cipher); response = await this.apiService.postCipherAdmin(request); + const data = new CipherData(response, cipher.collectionIds); + return new Cipher(data); } else if (cipher.collectionIds != null) { const request = new CipherCreateRequest(cipher); response = await this.apiService.postCipherCreate(request); @@ -697,6 +699,8 @@ export class CipherService implements CipherServiceAbstraction { if (orgAdmin && isNotClone) { const request = new CipherRequest(cipher); response = await this.apiService.putCipherAdmin(cipher.id, request); + const data = new CipherData(response, cipher.collectionIds); + return new Cipher(data, cipher.localData); } else if (cipher.edit) { const request = new CipherRequest(cipher); response = await this.apiService.putCipher(cipher.id, request); diff --git a/libs/importer/spec/onepassword-1pux-importer.spec.ts b/libs/importer/spec/onepassword-1pux-importer.spec.ts index fe1b2801bdf..93de4be606b 100644 --- a/libs/importer/spec/onepassword-1pux-importer.spec.ts +++ b/libs/importer/spec/onepassword-1pux-importer.spec.ts @@ -56,6 +56,16 @@ describe("1Password 1Pux Importer", () => { const SecureNoteDataJson = JSON.stringify(SecureNoteData); const SanitizedExportJson = JSON.stringify(SanitizedExport); + it("should not import items with state 'archived'", async () => { + const importer = new OnePassword1PuxImporter(); + const archivedLoginData = LoginData; + archivedLoginData["accounts"][0]["vaults"][0]["items"][0]["state"] = "archived"; + const archivedDataJson = JSON.stringify(archivedLoginData); + const result = await importer.parse(archivedDataJson); + expect(result != null).toBe(true); + expect(result.ciphers.length).toBe(0); + }); + it("should parse login data", async () => { const importer = new OnePassword1PuxImporter(); const result = await importer.parse(LoginDataJson); diff --git a/libs/importer/spec/test-data/onepassword-1pux/api-credentials.ts b/libs/importer/spec/test-data/onepassword-1pux/api-credentials.ts index 49269e72fa0..7a830194b8c 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/api-credentials.ts +++ b/libs/importer/spec/test-data/onepassword-1pux/api-credentials.ts @@ -26,7 +26,7 @@ export const APICredentialsData: ExportData = { favIndex: 0, createdAt: 1619465969, updatedAt: 1619466052, - trashed: false, + state: "active", categoryUuid: "112", details: { loginFields: [], @@ -41,7 +41,6 @@ export const APICredentialsData: ExportData = { value: { string: "apiuser@nullvalue.test", }, - indexAtSource: 0, guarded: true, multiline: false, dontGenerate: false, @@ -57,7 +56,6 @@ export const APICredentialsData: ExportData = { value: { concealed: "apiapiapiapiapiapiappy", }, - indexAtSource: 1, guarded: true, multiline: false, dontGenerate: true, @@ -73,7 +71,6 @@ export const APICredentialsData: ExportData = { value: { menu: "jwt", }, - indexAtSource: 2, guarded: false, multiline: false, dontGenerate: false, @@ -89,7 +86,6 @@ export const APICredentialsData: ExportData = { value: { string: "filename.jwt", }, - indexAtSource: 3, guarded: false, multiline: false, dontGenerate: false, @@ -105,7 +101,6 @@ export const APICredentialsData: ExportData = { value: { date: 1301918460, }, - indexAtSource: 4, guarded: false, multiline: false, dontGenerate: false, @@ -121,7 +116,6 @@ export const APICredentialsData: ExportData = { value: { date: 1932811260, }, - indexAtSource: 5, guarded: false, multiline: false, dontGenerate: false, @@ -137,7 +131,6 @@ export const APICredentialsData: ExportData = { value: { string: "not.your.everyday.hostname", }, - indexAtSource: 6, guarded: false, multiline: false, dontGenerate: false, diff --git a/libs/importer/spec/test-data/onepassword-1pux/bank-account.ts b/libs/importer/spec/test-data/onepassword-1pux/bank-account.ts index 7f3d8c080cd..6ffccafe514 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/bank-account.ts +++ b/libs/importer/spec/test-data/onepassword-1pux/bank-account.ts @@ -26,7 +26,7 @@ export const BankAccountData: ExportData = { favIndex: 0, createdAt: 1619466056, updatedAt: 1619466187, - trashed: false, + state: "active", categoryUuid: "101", details: { loginFields: [], @@ -41,7 +41,6 @@ export const BankAccountData: ExportData = { value: { string: "Super Credit Union", }, - indexAtSource: 0, guarded: false, multiline: false, dontGenerate: false, @@ -57,7 +56,6 @@ export const BankAccountData: ExportData = { value: { string: "Cool Guy", }, - indexAtSource: 1, guarded: false, multiline: false, dontGenerate: false, @@ -73,7 +71,6 @@ export const BankAccountData: ExportData = { value: { menu: "checking", }, - indexAtSource: 2, guarded: false, multiline: false, dontGenerate: false, @@ -89,7 +86,6 @@ export const BankAccountData: ExportData = { value: { string: "111000999", }, - indexAtSource: 3, guarded: false, multiline: false, dontGenerate: false, @@ -105,7 +101,6 @@ export const BankAccountData: ExportData = { value: { string: "192837465918273645", }, - indexAtSource: 4, guarded: false, multiline: false, dontGenerate: false, @@ -121,7 +116,6 @@ export const BankAccountData: ExportData = { value: { string: "123456", }, - indexAtSource: 5, guarded: false, multiline: false, dontGenerate: false, @@ -137,7 +131,6 @@ export const BankAccountData: ExportData = { value: { string: "DE12 123456", }, - indexAtSource: 6, guarded: false, multiline: false, dontGenerate: false, @@ -153,7 +146,6 @@ export const BankAccountData: ExportData = { value: { concealed: "5555", }, - indexAtSource: 7, guarded: false, multiline: false, dontGenerate: true, @@ -175,7 +167,6 @@ export const BankAccountData: ExportData = { value: { phone: "9399399933", }, - indexAtSource: 0, guarded: false, multiline: false, dontGenerate: false, @@ -191,7 +182,6 @@ export const BankAccountData: ExportData = { value: { string: "1 Fifth Avenue", }, - indexAtSource: 1, guarded: false, multiline: false, dontGenerate: false, diff --git a/libs/importer/spec/test-data/onepassword-1pux/credit-card.ts b/libs/importer/spec/test-data/onepassword-1pux/credit-card.ts index 80258f13f05..74af97a4c2b 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/credit-card.ts +++ b/libs/importer/spec/test-data/onepassword-1pux/credit-card.ts @@ -26,7 +26,7 @@ export const CreditCardData: ExportData = { favIndex: 0, createdAt: 1619465282, updatedAt: 1619465447, - trashed: false, + state: "active", categoryUuid: "002", details: { loginFields: [], @@ -41,7 +41,6 @@ export const CreditCardData: ExportData = { value: { string: "Fred Engels", }, - indexAtSource: 0, guarded: true, multiline: false, dontGenerate: false, @@ -57,7 +56,6 @@ export const CreditCardData: ExportData = { value: { creditCardType: "discover", }, - indexAtSource: 1, guarded: true, multiline: false, dontGenerate: false, @@ -73,7 +71,6 @@ export const CreditCardData: ExportData = { value: { creditCardNumber: "6011111111111117", }, - indexAtSource: 2, guarded: true, clipboardFilter: "0123456789", multiline: false, @@ -90,7 +87,6 @@ export const CreditCardData: ExportData = { value: { concealed: "1312", }, - indexAtSource: 3, guarded: true, multiline: false, dontGenerate: true, @@ -106,7 +102,6 @@ export const CreditCardData: ExportData = { value: { monthYear: 209912, }, - indexAtSource: 4, guarded: true, multiline: false, dontGenerate: false, @@ -122,7 +117,6 @@ export const CreditCardData: ExportData = { value: { monthYear: 200101, }, - indexAtSource: 5, guarded: true, multiline: false, dontGenerate: false, @@ -138,7 +132,6 @@ export const CreditCardData: ExportData = { value: { string: "card", }, - indexAtSource: 6, guarded: false, multiline: false, dontGenerate: false, @@ -160,7 +153,6 @@ export const CreditCardData: ExportData = { value: { string: "Some bank", }, - indexAtSource: 0, guarded: false, multiline: false, dontGenerate: false, @@ -176,7 +168,6 @@ export const CreditCardData: ExportData = { value: { phone: "123456", }, - indexAtSource: 1, guarded: false, multiline: false, dontGenerate: false, @@ -192,7 +183,6 @@ export const CreditCardData: ExportData = { value: { phone: "0800123456", }, - indexAtSource: 2, guarded: false, multiline: false, dontGenerate: false, @@ -208,7 +198,6 @@ export const CreditCardData: ExportData = { value: { phone: "+49123456", }, - indexAtSource: 3, guarded: false, multiline: false, dontGenerate: false, @@ -224,7 +213,6 @@ export const CreditCardData: ExportData = { value: { url: "somebank.com", }, - indexAtSource: 4, guarded: false, multiline: false, dontGenerate: false, @@ -246,7 +234,6 @@ export const CreditCardData: ExportData = { value: { concealed: "1234", }, - indexAtSource: 0, guarded: false, multiline: false, dontGenerate: true, @@ -262,7 +249,6 @@ export const CreditCardData: ExportData = { value: { string: "$1312", }, - indexAtSource: 1, guarded: false, multiline: false, dontGenerate: false, @@ -278,7 +264,6 @@ export const CreditCardData: ExportData = { value: { string: "$500", }, - indexAtSource: 2, guarded: false, multiline: false, dontGenerate: false, @@ -294,7 +279,6 @@ export const CreditCardData: ExportData = { value: { string: "1%", }, - indexAtSource: 3, guarded: false, multiline: false, dontGenerate: false, @@ -310,7 +294,6 @@ export const CreditCardData: ExportData = { value: { string: "123456", }, - indexAtSource: 4, guarded: false, multiline: false, dontGenerate: false, diff --git a/libs/importer/spec/test-data/onepassword-1pux/database.ts b/libs/importer/spec/test-data/onepassword-1pux/database.ts index 17b6700bf6d..58631a2f98b 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/database.ts +++ b/libs/importer/spec/test-data/onepassword-1pux/database.ts @@ -26,7 +26,7 @@ export const DatabaseData: ExportData = { favIndex: 0, createdAt: 1619466193, updatedAt: 1619466276, - trashed: false, + state: "active", categoryUuid: "102", details: { loginFields: [], @@ -41,7 +41,6 @@ export const DatabaseData: ExportData = { value: { menu: "postgresql", }, - indexAtSource: 0, guarded: false, multiline: false, dontGenerate: false, @@ -57,7 +56,6 @@ export const DatabaseData: ExportData = { value: { string: "my.secret.db.server", }, - indexAtSource: 1, guarded: false, multiline: false, dontGenerate: false, @@ -73,7 +71,6 @@ export const DatabaseData: ExportData = { value: { string: "1337", }, - indexAtSource: 2, guarded: false, multiline: false, dontGenerate: false, @@ -89,7 +86,6 @@ export const DatabaseData: ExportData = { value: { string: "user_database", }, - indexAtSource: 3, guarded: false, multiline: false, dontGenerate: false, @@ -105,7 +101,6 @@ export const DatabaseData: ExportData = { value: { string: "cooldbuser", }, - indexAtSource: 4, guarded: false, multiline: false, dontGenerate: false, @@ -121,7 +116,6 @@ export const DatabaseData: ExportData = { value: { concealed: "^+kTjhLaN7wVPAhGU)*J", }, - indexAtSource: 5, guarded: false, multiline: false, dontGenerate: false, @@ -137,7 +131,6 @@ export const DatabaseData: ExportData = { value: { string: "ASDIUFU-283234", }, - indexAtSource: 6, guarded: false, multiline: false, dontGenerate: false, @@ -153,7 +146,6 @@ export const DatabaseData: ExportData = { value: { string: "cdbu", }, - indexAtSource: 7, guarded: false, multiline: false, dontGenerate: false, @@ -169,7 +161,6 @@ export const DatabaseData: ExportData = { value: { string: "ssh", }, - indexAtSource: 8, guarded: false, multiline: false, dontGenerate: false, diff --git a/libs/importer/spec/test-data/onepassword-1pux/drivers-license.ts b/libs/importer/spec/test-data/onepassword-1pux/drivers-license.ts index 9f7260d63d3..54a52f36999 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/drivers-license.ts +++ b/libs/importer/spec/test-data/onepassword-1pux/drivers-license.ts @@ -26,7 +26,7 @@ export const DriversLicenseData: ExportData = { favIndex: 0, createdAt: 1619466279, updatedAt: 1619466425, - trashed: false, + state: "active", categoryUuid: "103", details: { loginFields: [], @@ -41,7 +41,6 @@ export const DriversLicenseData: ExportData = { value: { string: "Michael Scarn", }, - indexAtSource: 0, guarded: false, multiline: false, dontGenerate: false, @@ -57,7 +56,6 @@ export const DriversLicenseData: ExportData = { value: { string: "2120 Mifflin Rd.", }, - indexAtSource: 1, guarded: false, multiline: false, dontGenerate: false, @@ -73,7 +71,6 @@ export const DriversLicenseData: ExportData = { value: { date: 252504060, }, - indexAtSource: 2, guarded: false, multiline: false, dontGenerate: false, @@ -89,7 +86,6 @@ export const DriversLicenseData: ExportData = { value: { gender: "male", }, - indexAtSource: 3, guarded: false, multiline: false, dontGenerate: false, @@ -105,7 +101,6 @@ export const DriversLicenseData: ExportData = { value: { string: "5'11\"", }, - indexAtSource: 4, guarded: false, multiline: false, dontGenerate: false, @@ -121,7 +116,6 @@ export const DriversLicenseData: ExportData = { value: { string: "12345678901", }, - indexAtSource: 5, guarded: false, multiline: false, dontGenerate: false, @@ -137,7 +131,6 @@ export const DriversLicenseData: ExportData = { value: { string: "C", }, - indexAtSource: 6, guarded: false, multiline: false, dontGenerate: false, @@ -153,7 +146,6 @@ export const DriversLicenseData: ExportData = { value: { string: "B", }, - indexAtSource: 7, guarded: false, multiline: false, dontGenerate: false, @@ -169,7 +161,6 @@ export const DriversLicenseData: ExportData = { value: { string: "Pennsylvania", }, - indexAtSource: 8, guarded: false, multiline: false, dontGenerate: false, @@ -185,7 +176,6 @@ export const DriversLicenseData: ExportData = { value: { string: "United States", }, - indexAtSource: 9, guarded: false, multiline: false, dontGenerate: false, @@ -201,7 +191,6 @@ export const DriversLicenseData: ExportData = { value: { monthYear: 203012, }, - indexAtSource: 10, guarded: false, multiline: false, dontGenerate: false, diff --git a/libs/importer/spec/test-data/onepassword-1pux/email-account.ts b/libs/importer/spec/test-data/onepassword-1pux/email-account.ts index a6de6b55b22..0d94973ec29 100644 --- a/libs/importer/spec/test-data/onepassword-1pux/email-account.ts +++ b/libs/importer/spec/test-data/onepassword-1pux/email-account.ts @@ -26,7 +26,7 @@ export const EmailAccountData: ExportData = { favIndex: 0, createdAt: 1619466428, updatedAt: 1619466585, - trashed: false, + state: "active", categoryUuid: "111", details: { loginFields: [], @@ -41,7 +41,6 @@ export const EmailAccountData: ExportData = { value: { menu: "either", }, - indexAtSource: 0, guarded: false, multiline: false, dontGenerate: false, @@ -57,7 +56,6 @@ export const EmailAccountData: ExportData = { value: { string: "someuser@nullvalue.test", }, - indexAtSource: 1, guarded: false, multiline: false, dontGenerate: false, @@ -73,7 +71,6 @@ export const EmailAccountData: ExportData = { value: { string: "mailserver.nullvalue.test", }, - indexAtSource: 2, guarded: false, multiline: false, dontGenerate: false, @@ -89,7 +86,6 @@ export const EmailAccountData: ExportData = { value: { string: "587", }, - indexAtSource: 3, guarded: false, multiline: false, dontGenerate: false, @@ -105,7 +101,6 @@ export const EmailAccountData: ExportData = { value: { concealed: "u1jsf 0 && urlEncoded[0] === 33 // 33 = '!' + ? // URL is encrypted + await this.cryptoUtils.decryptAes256PlainWithDefault( + urlEncoded, + encryptionKey, + placeholder, + ) + : // URL is not encrypted + Utils.fromBufferToUtf8(this.decodeHexLoose(Utils.fromBufferToUtf8(urlEncoded))); // Ignore "group" accounts. They have no credentials. if (url == "http://group") { diff --git a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts index 34af24fe228..f670169400b 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts @@ -37,7 +37,7 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer { // const personalVaults = account.vaults[0].filter((v) => v.attrs.type === VaultAttributeTypeEnum.Personal); account.vaults.forEach((vault: VaultsEntity) => { vault.items.forEach((item: Item) => { - if (item.trashed === true) { + if (item.state === "archived") { return; } diff --git a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts index ebb52386545..353fcf1337e 100644 --- a/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts +++ b/libs/importer/src/importers/onepassword/types/onepassword-1pux-importer-types.ts @@ -53,7 +53,7 @@ export interface Item { favIndex: number; createdAt: number; updatedAt: number; - trashed?: boolean; + state: "active" | "archived"; categoryUuid: string; details: Details; overview: Overview; @@ -88,12 +88,12 @@ export interface SectionsEntity { title: string; name?: string | null; fields?: FieldsEntity[] | null; + hideAddAnotherField?: boolean | null; } export interface FieldsEntity { title: string; id: string; value: Value; - indexAtSource: number; guarded: boolean; multiline: boolean; dontGenerate: boolean; @@ -153,6 +153,8 @@ export interface Overview { pbe?: number | null; pgrng?: boolean | null; tags?: string[] | null; + icons?: string | null; + watchtowerExclusions?: string | null; } export interface UrlsEntity { label: string; diff --git a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts index 25c0e411243..28c18b3665f 100644 --- a/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/abstractions/cipher-form-config.service.ts @@ -79,6 +79,9 @@ type BaseCipherFormConfig = { * List of organizations that the user can create ciphers for. */ organizations?: Organization[]; + + /** Hides the fields that are only applicable to individuals, useful in the Admin Console where folders aren't applicable */ + hideIndividualVaultFields?: true; }; /** diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html index 26f7f7fd9e8..6c6bd8a801e 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html @@ -2,6 +2,7 @@

{{ "itemDetails" | i18n }}