diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index bf82c1bb74b..426e07948b2 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -704,6 +704,7 @@ dependencies = [ "windows 0.58.0", "zbus", "zbus_polkit", + "zeroizing-alloc", ] [[package]] @@ -3227,6 +3228,12 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zeroizing-alloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebff5e6b81c1c7dca2d0bd333b2006da48cb37dbcae5a8da888f31fcb3c19934" + [[package]] name = "zvariant" version = "4.2.0" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index b3883506c1f..d1be9d985da 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -60,6 +60,7 @@ rand_chacha = "=0.3.1" pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] } rsa = "=0.9.6" ed25519 = { version = "=2.2.3", features = ["pkcs8"] } +zeroizing-alloc = "0.1.0" [target.'cfg(windows)'.dependencies] widestring = { version = "=1.1.0", optional = true } diff --git a/apps/desktop/desktop_native/core/src/epheremal_values/mod.rs b/apps/desktop/desktop_native/core/src/epheremal_values/mod.rs new file mode 100644 index 00000000000..4d7ac4f4fea --- /dev/null +++ b/apps/desktop/desktop_native/core/src/epheremal_values/mod.rs @@ -0,0 +1,26 @@ +use std::collections::HashMap; + +#[derive(Clone)] +pub struct EpheremalValueStore { + values: HashMap, +} + +impl EpheremalValueStore { + pub fn new() -> Self { + EpheremalValueStore { + values: HashMap::new(), + } + } + + pub fn set(&mut self, key: String, value: String) { + self.values.insert(key, value); + } + + pub fn get(&self, key: &str) -> Option<&String> { + self.values.get(key) + } + + pub fn remove(&mut self, key: &str) { + self.values.remove(key); + } +} diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index f38e6ef97b4..74f5e1423d5 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -1,3 +1,8 @@ +pub use zeroizing_alloc::ZeroAlloc as ZeroizingAllocator; + +#[global_allocator] +static ALLOC: ZeroizingAllocator = ZeroizingAllocator(std::alloc::System); + #[cfg(feature = "sys")] pub mod biometric; #[cfg(feature = "sys")] @@ -12,5 +17,6 @@ pub mod process_isolation; #[cfg(feature = "sys")] pub mod powermonitor; #[cfg(feature = "sys")] - pub mod ssh_agent; +#[cfg(feature = "sys")] +pub mod epheremal_values; diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 62e448d4800..415640e1c34 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -123,3 +123,15 @@ export declare namespace ipc { send(message: string): number } } +export declare namespace epheremal_values { + export class EpheremalValueStoreWrapper { + /** Create a new epheremal value store. */ + constructor() + /** Set a value in the store. */ + set(key: string, value: string): void + /** Get a value from the store. */ + get(key: string): string | null + /** Remove a value from the store. */ + remove(key: string): void + } +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 8e156eb3efa..3260b826caa 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -522,3 +522,43 @@ pub mod ipc { } } } + +#[napi] +pub mod epheremal_values { + use desktop_core::epheremal_values::EpheremalValueStore; + use std::collections::HashMap; + + #[napi] + pub struct EpheremalValueStoreWrapper { + store: EpheremalValueStore, + } + + #[napi] + impl EpheremalValueStoreWrapper { + /// Create a new epheremal value store. + #[napi(constructor)] + pub fn new() -> napi::Result { + Ok(EpheremalValueStoreWrapper { + store: EpheremalValueStore::new(), + }) + } + + /// Set a value in the store. + #[napi] + pub fn set(&mut self, key: String, value: String) { + self.store.set(key, value); + } + + /// Get a value from the store. + #[napi] + pub fn get(&self, key: String) -> Option { + self.store.get(&key).cloned() + } + + /// Remove a value from the store. + #[napi] + pub fn remove(&mut self, key: String) { + self.store.remove(&key); + } + } +} \ No newline at end of file diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 040102d0395..cfb930f9480 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -42,7 +42,10 @@ import { AuthService, AuthService as AuthServiceAbstraction, } from "@bitwarden/common/auth/abstractions/auth.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ClientType } from "@bitwarden/common/enums"; @@ -90,6 +93,7 @@ import { import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; +import { DesktopPinService } from "../../auth/services/desktop-pin.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service"; import { flagEnabled } from "../../platform/flags"; @@ -224,12 +228,12 @@ const safeProviders: SafeProvider[] = [ provide: ProcessReloadServiceAbstraction, useClass: DefaultProcessReloadService, deps: [ - PinServiceAbstraction, MessagingServiceAbstraction, RELOAD_CALLBACK, VaultTimeoutSettingsService, BiometricStateService, AccountServiceAbstraction, + LogService, ], }), safeProvider({ @@ -358,6 +362,21 @@ const safeProviders: SafeProvider[] = [ useClass: DesktopLoginApprovalComponentService, deps: [I18nServiceAbstraction], }), + safeProvider({ + provide: PinServiceAbstraction, + useClass: DesktopPinService, + deps: [ + AccountServiceAbstraction, + CryptoFunctionServiceAbstraction, + EncryptService, + KdfConfigService, + KeyGenerationServiceAbstraction, + LogService, + MasterPasswordServiceAbstraction, + StateProvider, + StateServiceAbstraction, + ], + }), ]; @NgModule({ diff --git a/apps/desktop/src/auth/services/desktop-pin.service.ts b/apps/desktop/src/auth/services/desktop-pin.service.ts new file mode 100644 index 00000000000..e62fd4fa739 --- /dev/null +++ b/apps/desktop/src/auth/services/desktop-pin.service.ts @@ -0,0 +1,29 @@ +import { PinService } from "@bitwarden/auth/common"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { UserId } from "@bitwarden/common/types/guid"; + +export class DesktopPinService extends PinService { + async getPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise { + const epheremalValue = await ipc.platform.ephemeralStore.getEphemeralValue( + `pinKeyEncryptedUserKeyEphemeral-${userId}`, + ); + if (epheremalValue == null) { + return null; + } else { + return new EncString(epheremalValue); + } + } + + async setPinKeyEncryptedUserKeyEphemeral(value: EncString, userId: UserId): Promise { + return await ipc.platform.ephemeralStore.setEphemeralValue( + `pinKeyEncryptedUserKeyEphemeral-${userId}`, + value.encryptedString, + ); + } + + async deletePinKeyEncryptedUserKeyEphemeral(userId: string): Promise { + return await ipc.platform.ephemeralStore.removeEphemeralValue( + `pinKeyEncryptedUserKeyEphemeral-${userId}`, + ); + } +} diff --git a/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts b/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts index b59b48be1e1..9395b8352a2 100644 --- a/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts +++ b/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts @@ -1,21 +1,22 @@ import { ipcMain } from "electron"; +import { epheremal_values } from "@bitwarden/desktop-napi"; + /** * The ephemeral value store holds values that should be accessible to the renderer past a process reload. * In the current state, this store must not contain any keys that can decrypt a vault by themselves. */ export class EphemeralValueStorageService { - private ephemeralValues = new Map(); - constructor() { + const ephemeralValues = new epheremal_values.EpheremalValueStoreWrapper(); ipcMain.handle("setEphemeralValue", async (event, { key, value }) => { - this.ephemeralValues.set(key, value); + ephemeralValues.set(key, value); }); ipcMain.handle("getEphemeralValue", async (event, key: string) => { - return this.ephemeralValues.get(key); + return ephemeralValues.get(key); }); ipcMain.handle("deleteEphemeralValue", async (event, key: string) => { - this.ephemeralValues.delete(key); + ephemeralValues.remove(key); }); } } diff --git a/libs/auth/src/common/services/pin/pin.service.implementation.ts b/libs/auth/src/common/services/pin/pin.service.implementation.ts index 0f3d16fb625..204054ef8f0 100644 --- a/libs/auth/src/common/services/pin/pin.service.implementation.ts +++ b/libs/auth/src/common/services/pin/pin.service.implementation.ts @@ -148,7 +148,7 @@ export class PinService implements PinServiceAbstraction { /** * Sets the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey. */ - private async setPinKeyEncryptedUserKeyEphemeral( + protected async setPinKeyEncryptedUserKeyEphemeral( pinKeyEncryptedUserKey: EncString, userId: UserId, ): Promise { diff --git a/libs/common/src/key-management/services/default-process-reload.service.ts b/libs/common/src/key-management/services/default-process-reload.service.ts index 63082493622..5c38fd11330 100644 --- a/libs/common/src/key-management/services/default-process-reload.service.ts +++ b/libs/common/src/key-management/services/default-process-reload.service.ts @@ -1,9 +1,9 @@ import { firstValueFrom, map, timeout } from "rxjs"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { BiometricStateService } from "@bitwarden/key-management"; -import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; @@ -16,15 +16,16 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract private reloadInterval: any = null; constructor( - private pinService: PinServiceAbstraction, private messagingService: MessagingService, private reloadCallback: () => Promise = null, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, private accountService: AccountService, + private logService: LogService, ) {} async startProcessReload(authService: AuthService): Promise { + this.logService.info("[ProcessReloadService] Starting process reload"); const accounts = await firstValueFrom(this.accountService.accounts$); if (accounts != null) { const keys = Object.keys(accounts); @@ -33,6 +34,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); status = await authService.getAuthStatus(userId); if (status === AuthenticationStatus.Unlocked) { + this.logService.info( + `[ProcessReloadService] User ${userId} is unlocked, skipping process reload`, + ); return; } } @@ -41,18 +45,10 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract // A reloadInterval has already been set and is executing if (this.reloadInterval != null) { + this.logService.info(`[ProcessReloadService] Process reload already in progress`); return; } - // If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock. - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - if (userId != null) { - const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); - if (ephemeralPin != null) { - return; - } - } - this.cancelProcessReload(); await this.executeProcessReload(); } @@ -86,6 +82,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract } } + this.logService.info("[ProcessReloadService] Sending message to os reload implementation"); this.messagingService.send("reloadProcess"); if (this.reloadCallback != null) { await this.reloadCallback(); @@ -93,6 +90,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract return; } if (this.reloadInterval == null) { + this.logService.info("[ProcessReloadService] Setting reload interval"); this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); } }