Skip to content

Commit 04484dd

Browse files
theoephraimclaude
andcommitted
add process verification and memory protection for key material
Security: - Verify connecting client process via GetNamedPipeClientProcessId + QueryFullProcessImageName — only allow varlock/node/bun binaries - Add SecureBytes wrapper: VirtualLock/mlock prevents key material from being swapped to disk, zeroize-on-drop clears secrets from memory - Apply SecureBytes to private keys in daemon decrypt and one-shot decrypt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ab2c8ae commit 04484dd

6 files changed

Lines changed: 176 additions & 6 deletions

File tree

packages/encryption-binary-rust/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/encryption-binary-rust/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ serde = { version = "1", features = ["derive"] }
2323
serde_json = "1"
2424
base64 = "0.22"
2525

26+
# Memory safety
27+
zeroize = "1"
28+
2629
# Signal handling (Unix)
2730
[target.'cfg(unix)'.dependencies]
2831
libc = "0.2"
@@ -42,9 +45,11 @@ windows = { version = "0.58", features = [
4245
"Win32_Storage_FileSystem",
4346
"Win32_Foundation",
4447
"Win32_System_IO",
45-
# Pipe ACL (restrict to current user)
48+
# Pipe ACL (restrict to current user) + process verification
4649
"Win32_Security",
4750
"Win32_Security_Authorization",
51+
"Win32_System_Threading",
52+
"Win32_System_Memory",
4853
# Windows Hello (UserConsentVerifier)
4954
"Security_Credentials_UI",
5055
"Foundation",

packages/encryption-binary-rust/src/daemon.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,16 @@ fn handle_decrypt(
204204
}
205205
}
206206

207-
// Load key and decrypt
207+
// Load key and decrypt — private key is held in locked, zeroize-on-drop memory
208208
match key_store::load_key(key_id) {
209209
Ok((private_key_der, public_key_b64)) => {
210+
let secure_key = crate::secure_mem::SecureBytes::new(private_key_der);
210211
let private_key_b64 = base64::Engine::encode(
211212
&base64::engine::general_purpose::STANDARD,
212-
&private_key_der,
213+
secure_key.as_slice(),
213214
);
214215

215-
match crypto::decrypt(&private_key_b64, &public_key_b64, ciphertext_b64) {
216+
let result = match crypto::decrypt(&private_key_b64, &public_key_b64, ciphertext_b64) {
216217
Ok(plaintext_bytes) => {
217218
match String::from_utf8(plaintext_bytes) {
218219
Ok(plaintext) => {
@@ -226,7 +227,9 @@ fn handle_decrypt(
226227
}
227228
}
228229
Err(e) => json!({"error": e}),
229-
}
230+
};
231+
drop(secure_key); // explicit drop zeroizes + unlocks
232+
result
230233
}
231234
Err(e) => json!({"error": e}),
232235
}

packages/encryption-binary-rust/src/ipc.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,17 @@ impl IpcServer {
196196
std::thread::spawn(move || {
197197
use windows::Win32::Foundation::HANDLE;
198198
let pipe = HANDLE(raw_handle as *mut _);
199+
200+
// Verify the connecting process is a trusted varlock binary
201+
if !verify_client_process(pipe) {
202+
eprintln!("Rejected connection from untrusted process");
203+
unsafe {
204+
let _ = DisconnectNamedPipe(pipe);
205+
let _ = CloseHandle(pipe);
206+
}
207+
return;
208+
}
209+
199210
handle_windows_client(pipe, handler, on_activity, running, tty_id);
200211
unsafe {
201212
let _ = DisconnectNamedPipe(pipe);
@@ -453,6 +464,74 @@ fn create_current_user_security_attributes() -> Result<windows::Win32::Security:
453464
}
454465
}
455466

467+
// ── Windows client process verification ─────────────────────────
468+
469+
/// Verify that the connecting client process is a trusted varlock binary.
470+
/// Uses GetNamedPipeClientProcessId to get the client PID, then checks
471+
/// that the process executable matches our own binary name.
472+
#[cfg(windows)]
473+
fn verify_client_process(pipe: windows::Win32::Foundation::HANDLE) -> bool {
474+
use windows::Win32::System::Pipes::GetNamedPipeClientProcessId;
475+
use windows::Win32::System::Threading::{
476+
OpenProcess, QueryFullProcessImageNameW, PROCESS_QUERY_LIMITED_INFORMATION,
477+
PROCESS_NAME_WIN32,
478+
};
479+
use windows::Win32::Foundation::CloseHandle;
480+
481+
// Get the client's PID
482+
let mut client_pid = 0u32;
483+
let ok = unsafe { GetNamedPipeClientProcessId(pipe, &mut client_pid) };
484+
if ok.is_err() || client_pid == 0 {
485+
return false;
486+
}
487+
488+
// Open the client process to query its image name
489+
let process = unsafe {
490+
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, client_pid)
491+
};
492+
let process = match process {
493+
Ok(h) => h,
494+
Err(_) => return false,
495+
};
496+
497+
// Query the full executable path
498+
let mut buf = [0u16; 1024];
499+
let mut len = buf.len() as u32;
500+
let ok = unsafe {
501+
QueryFullProcessImageNameW(process, PROCESS_NAME_WIN32, windows::core::PWSTR(buf.as_mut_ptr()), &mut len)
502+
};
503+
unsafe { let _ = CloseHandle(process); }
504+
505+
if ok.is_err() || len == 0 {
506+
return false;
507+
}
508+
509+
let client_path = String::from_utf16_lossy(&buf[..len as usize]);
510+
511+
// Extract the filename from the full path
512+
let client_filename = client_path
513+
.rsplit('\\')
514+
.next()
515+
.unwrap_or("")
516+
.to_lowercase();
517+
518+
// Allow connections from varlock binaries and Node.js (for native Windows daemon client)
519+
let allowed = [
520+
"varlock-local-encrypt.exe",
521+
"varlock.exe",
522+
"node.exe", // Node.js daemon client on native Windows
523+
"bun.exe", // Bun runtime on native Windows
524+
];
525+
526+
let is_trusted = allowed.iter().any(|name| client_filename == *name);
527+
if !is_trusted {
528+
eprintln!(
529+
"Untrusted client process: PID={client_pid}, path={client_path}"
530+
);
531+
}
532+
is_trusted
533+
}
534+
456535
// ── Windows named pipe client handling ───────────────────────────
457536

458537
#[cfg(windows)]

packages/encryption-binary-rust/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod daemon;
1010
mod daemon_client;
1111
mod ipc;
1212
mod key_store;
13+
mod secure_mem;
1314

1415
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
1516
use serde_json::json;
@@ -173,12 +174,14 @@ fn cmd_decrypt(args: &[String]) {
173174
}
174175

175176
// Direct decrypt (no biometric verification)
177+
// Private key is held in locked, zeroize-on-drop memory
176178
let (private_key_der, public_key_b64) = match key_store::load_key(&key_id) {
177179
Ok(k) => k,
178180
Err(e) => json_error(&e),
179181
};
180182

181-
let private_key_b64 = BASE64.encode(&private_key_der);
183+
let secure_key = secure_mem::SecureBytes::new(private_key_der);
184+
let private_key_b64 = BASE64.encode(secure_key.as_slice());
182185

183186
match crypto::decrypt(&private_key_b64, &public_key_b64, &data_b64) {
184187
Ok(plaintext_bytes) => {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//! Secure memory utilities for protecting sensitive key material.
2+
//!
3+
//! - Locks memory pages to prevent swapping to disk (VirtualLock / mlock)
4+
//! - Zeroizes memory on drop to prevent lingering secrets
5+
6+
use zeroize::Zeroize;
7+
8+
/// A Vec<u8> wrapper that locks its memory (prevents swapping) and
9+
/// zeroizes contents on drop.
10+
pub struct SecureBytes {
11+
inner: Vec<u8>,
12+
}
13+
14+
impl SecureBytes {
15+
/// Create a SecureBytes from existing data. Locks the memory region.
16+
pub fn new(data: Vec<u8>) -> Self {
17+
if !data.is_empty() {
18+
lock_memory(data.as_ptr(), data.len());
19+
}
20+
Self { inner: data }
21+
}
22+
23+
pub fn as_slice(&self) -> &[u8] {
24+
&self.inner
25+
}
26+
}
27+
28+
impl Drop for SecureBytes {
29+
fn drop(&mut self) {
30+
let ptr = self.inner.as_ptr();
31+
let len = self.inner.len();
32+
33+
// Zeroize the contents
34+
self.inner.zeroize();
35+
36+
// Unlock the memory region
37+
if len > 0 {
38+
unlock_memory(ptr, len);
39+
}
40+
}
41+
}
42+
43+
// ── Platform-specific memory locking ────────────────────────────
44+
45+
#[cfg(target_os = "windows")]
46+
fn lock_memory(ptr: *const u8, len: usize) {
47+
use windows::Win32::System::Memory::VirtualLock;
48+
unsafe {
49+
let _ = VirtualLock(ptr as *const _, len);
50+
}
51+
}
52+
53+
#[cfg(target_os = "windows")]
54+
fn unlock_memory(ptr: *const u8, len: usize) {
55+
use windows::Win32::System::Memory::VirtualUnlock;
56+
unsafe {
57+
let _ = VirtualUnlock(ptr as *const _, len);
58+
}
59+
}
60+
61+
#[cfg(unix)]
62+
fn lock_memory(ptr: *const u8, len: usize) {
63+
unsafe {
64+
libc::mlock(ptr as *const _, len);
65+
}
66+
}
67+
68+
#[cfg(unix)]
69+
fn unlock_memory(ptr: *const u8, len: usize) {
70+
unsafe {
71+
libc::munlock(ptr as *const _, len);
72+
}
73+
}
74+
75+
#[cfg(not(any(unix, target_os = "windows")))]
76+
fn lock_memory(_ptr: *const u8, _len: usize) {}
77+
78+
#[cfg(not(any(unix, target_os = "windows")))]
79+
fn unlock_memory(_ptr: *const u8, _len: usize) {}

0 commit comments

Comments
 (0)