use std::path::Path;
use std::time::{Duration, Instant};
use sha2::{Digest, Sha256};
use zeroize::Zeroizing;
use windows::core::{HRESULT, HSTRING, PWSTR};
use windows::Win32::Foundation::{
CloseHandle, GetLastError, LocalFree, ERROR_IO_PENDING, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED,
HANDLE, HLOCAL, INVALID_HANDLE_VALUE, WAIT_OBJECT_0,
};
use windows::Win32::Security::Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW, GetSecurityInfo,
SDDL_REVISION_1, SE_KERNEL_OBJECT,
};
use windows::Win32::Security::{
GetTokenInformation, TokenUser, OBJECT_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, PSID,
PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
};
use windows::Win32::Storage::FileSystem::{
CreateFileW, ReadFile, WriteFile, FILE_FLAGS_AND_ATTRIBUTES, FILE_GENERIC_READ,
FILE_GENERIC_WRITE, FILE_SHARE_NONE, OPEN_EXISTING,
};
use windows::Win32::System::Pipes::{
ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, GetNamedPipeClientProcessId,
WaitNamedPipeW, NAMED_PIPE_MODE, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_WAIT,
};
use windows::Win32::System::Threading::{
CreateEventW, OpenProcess, OpenProcessToken, ResetEvent, WaitForSingleObject,
PROCESS_QUERY_LIMITED_INFORMATION,
};
use windows::Win32::System::IO::{CancelIo, GetOverlappedResult, OVERLAPPED};
use crate::session::{
audit, decode_response, encode_verdict, handle_get, parse_request, Peer, Request,
};
const PIPE_ACCESS_DUPLEX: u32 = 0x0000_0003;
const FILE_FLAG_FIRST_PIPE_INSTANCE: u32 = 0x0008_0000;
const FILE_FLAG_OVERLAPPED: u32 = 0x4000_0000;
const SECURITY_SQOS_PRESENT: u32 = 0x0010_0000;
const SECURITY_IDENTIFICATION: u32 = 0x0001_0000;
const ACCEPT_TICK_MS: u32 = 200;
const IO_TIMEOUT_MS: u32 = 5_000;
const MAX_REQUEST: usize = 1024;
const BUSY_WAIT_MS: u32 = 3_000;
pub fn pipe_name(secrets_dir: &Path) -> String {
let sid = crate::winacl::current_user_sid().unwrap_or_else(|_| "unknown-sid".to_string());
let canon = std::fs::canonicalize(secrets_dir).unwrap_or_else(|_| secrets_dir.to_path_buf());
let mut h = Sha256::new();
h.update(canon.to_string_lossy().to_lowercase().as_bytes());
let digest = h.finalize();
let tag: String = digest[..8].iter().map(|b| format!("{b:02x}")).collect();
format!(r"\\.\pipe\io.quantumencoding.secrets\{sid}\{tag}")
}
struct OwnedHandle(HANDLE);
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.is_invalid() {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
}
struct Ov {
event: OwnedHandle,
ov: OVERLAPPED,
}
impl Ov {
fn new() -> Result<Self, String> {
let event = unsafe { CreateEventW(None, true, false, None) }
.map_err(|e| format!("CreateEvent: {e}"))?;
let mut ov = OVERLAPPED::default();
ov.hEvent = event;
Ok(Self {
event: OwnedHandle(event),
ov,
})
}
fn reset(&mut self) {
unsafe {
let _ = ResetEvent(self.event.0);
}
self.ov.Anonymous.Anonymous.Offset = 0;
self.ov.Anonymous.Anonymous.OffsetHigh = 0;
}
}
fn is_win32(e: &windows::core::Error, code: u32) -> bool {
e.code() == HRESULT::from_win32(code)
}
unsafe fn await_io(pipe: HANDLE, ov: &mut Ov, timeout_ms: u32) -> Result<Option<u32>, String> { unsafe {
if WaitForSingleObject(ov.event.0, timeout_ms) != WAIT_OBJECT_0 {
let _ = CancelIo(pipe);
return Ok(None);
}
let mut n = 0u32;
GetOverlappedResult(pipe, &ov.ov, &mut n, false)
.map_err(|e| format!("GetOverlappedResult: {e}"))?;
Ok(Some(n))
}}
unsafe fn pipe_read(pipe: HANDLE, ov: &mut Ov, buf: &mut [u8], timeout_ms: u32) -> Option<usize> { unsafe {
ov.reset();
match ReadFile(pipe, Some(buf), None, Some(&mut ov.ov)) {
Ok(()) => {}
Err(ref e) if is_win32(e, ERROR_IO_PENDING.0) => {}
Err(_) => return None,
}
match await_io(pipe, ov, timeout_ms) {
Ok(Some(n)) => Some(n as usize),
_ => None,
}
}}
unsafe fn pipe_write(pipe: HANDLE, ov: &mut Ov, buf: &[u8], timeout_ms: u32) -> bool { unsafe {
let mut written = 0usize;
while written < buf.len() {
ov.reset();
match WriteFile(pipe, Some(&buf[written..]), None, Some(&mut ov.ov)) {
Ok(()) => {}
Err(ref e) if is_win32(e, ERROR_IO_PENDING.0) => {}
Err(_) => return false,
}
match await_io(pipe, ov, timeout_ms) {
Ok(Some(0)) | Ok(None) | Err(_) => return false,
Ok(Some(n)) => written += n as usize,
}
}
true
}}
unsafe fn read_line(pipe: HANDLE, ov: &mut Ov) -> Vec<u8> { unsafe {
let mut out = Vec::with_capacity(64);
let mut chunk = [0u8; 256];
while out.len() < MAX_REQUEST {
match pipe_read(pipe, ov, &mut chunk, IO_TIMEOUT_MS) {
Some(0) | None => break,
Some(n) => {
for &b in &chunk[..n] {
if b == b'\n' {
return out;
}
out.push(b);
if out.len() >= MAX_REQUEST {
return out;
}
}
}
}
}
out
}}
fn sid_to_string(sid: PSID) -> Option<String> {
unsafe {
let mut s = PWSTR::null();
ConvertSidToStringSidW(sid, &mut s).ok()?;
let out = s.to_string().ok();
let _ = LocalFree(Some(HLOCAL(s.0 as *mut core::ffi::c_void)));
out
}
}
fn process_user_sid(proc: HANDLE) -> Option<String> {
unsafe {
let mut token = HANDLE::default();
OpenProcessToken(proc, TOKEN_QUERY, &mut token).ok()?;
let token = OwnedHandle(token);
let mut len = 0u32;
let _ = GetTokenInformation(token.0, TokenUser, None, 0, &mut len);
if len == 0 {
return None;
}
let mut buf = vec![0u8; len as usize];
GetTokenInformation(
token.0,
TokenUser,
Some(buf.as_mut_ptr() as *mut core::ffi::c_void),
len,
&mut len,
)
.ok()?;
let tu = &*(buf.as_ptr() as *const TOKEN_USER);
sid_to_string(tu.User.Sid)
}
}
fn peer_identity(pipe: HANDLE, our_sid: &str) -> Option<(Peer, OwnedHandle)> {
unsafe {
let mut pid: u32 = 0;
GetNamedPipeClientProcessId(pipe, &mut pid).ok()?;
let proc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).ok()?;
let proc = OwnedHandle(proc);
let peer_sid = process_user_sid(proc.0);
let same_user = peer_sid.as_deref() == Some(our_sid);
Some((
Peer {
pid: pid as i32,
same_user,
detail: "win-pipe".to_string(),
},
proc,
))
}
}
fn object_owner_sid(handle: HANDLE) -> Option<String> {
unsafe {
let mut owner = PSID::default();
let mut psd = PSECURITY_DESCRIPTOR::default();
let rc = GetSecurityInfo(
handle,
SE_KERNEL_OBJECT,
OBJECT_SECURITY_INFORMATION(OWNER_SECURITY_INFORMATION.0),
Some(&mut owner),
None,
None,
None,
Some(&mut psd),
);
if rc.is_err() {
return None;
}
let out = sid_to_string(owner);
if !psd.is_invalid() {
let _ = LocalFree(Some(HLOCAL(psd.0)));
}
out
}
}
fn connect_verified(secrets_dir: &Path) -> Result<Option<(OwnedHandle, Ov)>, String> {
let name = HSTRING::from(pipe_name(secrets_dir));
let handle = loop {
let h = unsafe {
CreateFileW(
&name,
(FILE_GENERIC_READ | FILE_GENERIC_WRITE).0,
FILE_SHARE_NONE,
None,
OPEN_EXISTING,
FILE_FLAGS_AND_ATTRIBUTES(
FILE_FLAG_OVERLAPPED | SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION,
),
None,
)
};
if std::env::var_os("SECRETS_DEBUG_BROKER").is_some() {
eprintln!(
"secrets[broker-debug]: client open {} -> {:?}",
pipe_name(secrets_dir),
h.as_ref().err().map(|e| e.code())
);
}
match h {
Ok(h) if !h.is_invalid() => break OwnedHandle(h),
Err(ref e) if is_win32(e, ERROR_PIPE_BUSY.0) => {
let waited = unsafe { WaitNamedPipeW(&name, BUSY_WAIT_MS).as_bool() };
if !waited {
return Ok(None);
}
}
_ => return Ok(None),
}
};
let ours = crate::winacl::current_user_sid()?;
match object_owner_sid(handle.0) {
Some(owner) if owner == ours => {}
other => {
return Err(format!(
"the session-broker pipe is owned by {} , not by you ({ours}).\n\
Refusing to send anything to it. Another process on this machine is \
squatting the vault's pipe name — treat this as hostile, not as a \
broken broker.",
other.as_deref().unwrap_or("an unknown account")
));
}
}
let ov = Ov::new()?;
Ok(Some((handle, ov)))
}
pub fn request_value(
secrets_dir: &Path,
project: &str,
key: &str,
) -> Option<Zeroizing<Vec<u8>>> {
let (handle, mut ov) = match connect_verified(secrets_dir) {
Ok(Some(x)) => x,
Ok(None) => return None,
Err(e) => {
eprintln!("secrets: {e}");
return None;
}
};
let req = format!("GET {project} {key}\n");
unsafe {
if !pipe_write(handle.0, &mut ov, req.as_bytes(), IO_TIMEOUT_MS) {
return None;
}
let mut buf = Zeroizing::new(Vec::new());
let mut chunk = [0u8; 512];
loop {
match pipe_read(handle.0, &mut ov, &mut chunk, IO_TIMEOUT_MS) {
Some(0) | None => break,
Some(n) => buf.extend_from_slice(&chunk[..n]),
}
if buf.len() > 64 * 1024 {
return None;
}
}
decode_response(&buf)
}
}
pub fn end(secrets_dir: &Path) {
if let Ok(Some((handle, mut ov))) = connect_verified(secrets_dir) {
unsafe {
let _ = pipe_write(handle.0, &mut ov, b"END\n", IO_TIMEOUT_MS);
}
}
}
fn create_pipe(name: &str, sid: &str) -> Result<OwnedHandle, String> {
let sddl = HSTRING::from(format!("D:P(A;;GA;;;{sid})"));
let wide = HSTRING::from(name);
unsafe {
let mut psd = PSECURITY_DESCRIPTOR::default();
ConvertStringSecurityDescriptorToSecurityDescriptorW(&sddl, SDDL_REVISION_1, &mut psd, None)
.map_err(|e| format!("building pipe security descriptor: {e}"))?;
let sa = SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: psd.0,
bInheritHandle: false.into(),
};
let h = CreateNamedPipeW(
&wide,
FILE_FLAGS_AND_ATTRIBUTES(
PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED,
),
NAMED_PIPE_MODE(PIPE_TYPE_BYTE.0 | PIPE_WAIT.0 | PIPE_REJECT_REMOTE_CLIENTS.0),
1, 4096,
4096,
0,
Some(&sa),
);
let _ = LocalFree(Some(HLOCAL(psd.0)));
if h == INVALID_HANDLE_VALUE || h.is_invalid() {
let err = GetLastError();
return Err(format!(
"could not create the broker pipe (WIN32_ERROR {}). \
If the name is already taken, another broker is running (or something \
is squatting it) — `secrets lock` ends a live one.",
err.0
));
}
Ok(OwnedHandle(h))
}
}
pub fn serve(secrets_dir: &Path, minutes: u64, pass: Zeroizing<String>) -> Result<(), String> {
use secrets_vault::{v2_salt, MasterSecret, VaultReader};
let vault_path = secrets_dir.join("vault.qvlt");
let data = std::fs::read(&vault_path).map_err(|e| format!("read vault: {e}"))?;
let salt = v2_salt(&data).map_err(|e| format!("not a v2 vault: {e}"))?;
let master = MasterSecret::derive(&pass, &salt);
drop(pass); let registry_key_z = master.registry_key();
let registry_key: &[u8; 32] = ®istry_key_z;
VaultReader::open(data, &master).map_err(|e| format!("vault open: {e}"))?;
let our_sid = crate::winacl::current_user_sid()?;
let name = pipe_name(secrets_dir);
let pipe = create_pipe(&name, &our_sid)?;
let mut ov = Ov::new()?;
audit(secrets_dir, &format!("PIPE {name}"));
audit(secrets_dir, &format!("START lifetime={minutes}m"));
let deadline = Instant::now() + Duration::from_secs(minutes.saturating_mul(60));
let mut armed = arm_connect(pipe.0, &mut ov)?;
loop {
if Instant::now() >= deadline {
break;
}
if !armed {
armed = arm_connect(pipe.0, &mut ov)?;
continue;
}
let connected = unsafe {
match await_io(pipe.0, &mut ov, ACCEPT_TICK_MS) {
Ok(Some(_)) => true,
Ok(None) => {
armed = false;
continue;
}
Err(_) => {
armed = false;
continue;
}
}
};
if !connected {
continue;
}
let identity = peer_identity(pipe.0, &our_sid);
let (peer, _pin) = match identity {
Some((p, pin)) => (Some(p), Some(pin)),
None => (None, None),
};
let line_bytes = unsafe { read_line(pipe.0, &mut ov) };
let line = String::from_utf8_lossy(&line_bytes);
let mut stop = false;
match parse_request(&line) {
Request::End => {
audit(secrets_dir, "END (requested)");
stop = true;
}
Request::Get { project, key } => {
let verdict = handle_get(
secrets_dir,
&vault_path,
&master,
registry_key,
peer,
project,
key,
);
let out = encode_verdict(verdict);
unsafe {
let _ = pipe_write(pipe.0, &mut ov, &out, IO_TIMEOUT_MS);
}
}
Request::LegacyGet => {
audit(secrets_dir, "LEGACY-GET (empty close)");
}
Request::Malformed => unsafe {
let _ = pipe_write(pipe.0, &mut ov, b"ERR denied\n", IO_TIMEOUT_MS);
},
}
unsafe {
let _ = DisconnectNamedPipe(pipe.0);
}
armed = false;
if stop {
break;
}
}
audit(secrets_dir, "STOP");
Ok(())
}
fn arm_connect(pipe: HANDLE, ov: &mut Ov) -> Result<bool, String> {
ov.reset();
unsafe {
match ConnectNamedPipe(pipe, Some(&mut ov.ov)) {
Ok(()) => {
let _ = windows::Win32::System::Threading::SetEvent(ov.event.0);
Ok(true)
}
Err(ref e) if is_win32(e, ERROR_IO_PENDING.0) => Ok(true),
Err(ref e) if is_win32(e, ERROR_PIPE_CONNECTED.0) => {
let _ = windows::Win32::System::Threading::SetEvent(ov.event.0);
Ok(true)
}
Err(_) => Ok(false),
}
}
}
pub fn probe(secrets_dir: &Path) -> crate::session::Probe {
use crate::session::Probe;
match connect_verified(secrets_dir) {
Ok(Some(_)) => Probe::Live,
Ok(None) => Probe::Absent,
Err(why) => Probe::Foreign(why),
}
}
pub fn detach() {}