1
0
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:
Bernd Schoolmann
2024-12-01 23:54:11 +01:00
parent f79141c421
commit d8da1a95d5
11 changed files with 159 additions and 20 deletions

View File

@@ -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"

View File

@@ -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 }

View 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);
}
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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({

View 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}`,
);
}
}

View File

@@ -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);
});
}
}

View File

@@ -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> {

View File

@@ -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);
}
}