mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 06:23:38 +00:00
Allow epheremal pin to survive process reload on desktop and add epheremal pin store for rust
This commit is contained in:
7
apps/desktop/desktop_native/Cargo.lock
generated
7
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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 }
|
||||
|
||||
26
apps/desktop/desktop_native/core/src/epheremal_values/mod.rs
Normal file
26
apps/desktop/desktop_native/core/src/epheremal_values/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EpheremalValueStore {
|
||||
values: HashMap<String, String>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
pub use zeroizing_alloc::ZeroAlloc as ZeroizingAllocator;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: ZeroizingAllocator<std::alloc::System> = 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;
|
||||
|
||||
12
apps/desktop/desktop_native/napi/index.d.ts
vendored
12
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Self> {
|
||||
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<String> {
|
||||
self.store.get(&key).cloned()
|
||||
}
|
||||
|
||||
/// Remove a value from the store.
|
||||
#[napi]
|
||||
pub fn remove(&mut self, key: String) {
|
||||
self.store.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
29
apps/desktop/src/auth/services/desktop-pin.service.ts
Normal file
29
apps/desktop/src/auth/services/desktop-pin.service.ts
Normal file
@@ -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<EncString> {
|
||||
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<void> {
|
||||
return await ipc.platform.ephemeralStore.setEphemeralValue(
|
||||
`pinKeyEncryptedUserKeyEphemeral-${userId}`,
|
||||
value.encryptedString,
|
||||
);
|
||||
}
|
||||
|
||||
async deletePinKeyEncryptedUserKeyEphemeral(userId: string): Promise<void> {
|
||||
return await ipc.platform.ephemeralStore.removeEphemeralValue(
|
||||
`pinKeyEncryptedUserKeyEphemeral-${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<string, string>();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
|
||||
@@ -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<void> = null,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private accountService: AccountService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async startProcessReload(authService: AuthService): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user