Publicētais avots
Lejupielādēt avota ZIP
CH-J Tools
Pārlūkot mapes un failus konkrētai lietojumprogrammas izdošanai.
CH-J / Server Manager
1704 faili
0.1.2-beta
Pievienots: · Būvēts:
1
//! OS-protected vault unlocking (port of `security/biometrics/*`).2
//!3
//! The 32-byte vault key never leaves the Core except to the isolated native helper4
//! (macOS Secure Enclave wrap with Touch ID, Windows Hello + DPAPI, Linux fprintd +5
//! libsecret). `security/biometrics.json` keeps the 0.1.0 format (schema 1, vaultId,6
//! entryId, provider, optional `wrapped`), so existing enrollments keep working.8
use crate::core::error::{CoreError, CoreResult};9
use crate::core::storage::atomic_write_json;10
use crate::core::vault::VaultStore;11
use base64::engine::general_purpose::STANDARD as B64;12
use base64::Engine;13
use parking_lot::Mutex;14
use serde_json::{json, Value};15
use std::path::{Path, PathBuf};16
use std::process::Stdio;17
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};18
use std::sync::Arc;19
use std::time::Duration;20
use tokio::io::{AsyncReadExt, AsyncWriteExt};21
use tokio::sync::Notify;22
use zeroize::Zeroizing;24
pub fn biometric_error(code: &str) -> CoreError {25
CoreError::new(code, code)26
}28
#[derive(Clone, Debug)]29
pub struct ProviderInfo {30
pub provider: &'static str,31
pub protection: &'static str,32
}34
pub fn provider_info() -> ProviderInfo {35
if cfg!(target_os = "macos") {36
ProviderInfo { provider: "touch-id", protection: "biometry-current-set" }37
} else if cfg!(windows) {38
ProviderInfo { provider: "windows-hello", protection: "convenience" }39
} else if cfg!(target_os = "linux") {40
ProviderInfo { provider: "fprintd", protection: "convenience" }41
} else {42
ProviderInfo { provider: "none", protection: "unavailable" }43
}44
}46
/// Native helper invocation (port of `nativeRunner.js`): JSON request on stdin, bounded47
/// JSON reply on stdout, stderr discarded, never logged.48
pub struct NativeRunner {49
executable: PathBuf,50
args: Vec<String>,51
timeout: Duration,52
}54
impl NativeRunner {55
pub fn for_resources(resources: &Path) -> Self {56
let root = resources.join("biometrics");57
if cfg!(target_os = "linux") {58
Self { executable: PathBuf::from("/usr/bin/python3"), args: vec![root.join("linux.py").to_string_lossy().to_string()], timeout: Duration::from_secs(65) }59
} else {60
Self { executable: root.join(if cfg!(windows) { "chj-biometric.exe" } else { "chj-biometric" }), args: Vec::new(), timeout: Duration::from_secs(65) }61
}62
}64
pub async fn run(&self, request: &Value, cancel: &Notify) -> CoreResult<Value> {65
let op = request["op"].as_str().unwrap_or("");66
let mut command = tokio::process::Command::new(&self.executable);67
command.args(&self.args).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).kill_on_drop(true);68
#[cfg(windows)]69
command.creation_flags(0x0800_0000);70
let mut child = command.spawn().map_err(|_| biometric_error("BIOMETRIC_UNAVAILABLE"))?;71
let input = Zeroizing::new(format!("{}\n", serde_json::to_string(request).unwrap_or_default()));72
if let Some(mut stdin) = child.stdin.take() {73
if stdin.write_all(input.as_bytes()).await.is_err() {74
return Err(biometric_error("BIOMETRIC_UNAVAILABLE"));75
}76
}77
drop(input);78
let mut stdout = child.stdout.take().ok_or_else(|| biometric_error("BIOMETRIC_UNAVAILABLE"))?;79
let timeout = if op == "status" { self.timeout.min(Duration::from_secs(8)) } else { self.timeout };80
let work = async {81
let mut buffer = Zeroizing::new(Vec::new());82
let mut chunk = [0u8; 4096];83
loop {84
let n = stdout.read(&mut chunk).await.map_err(|_| biometric_error("BIOMETRIC_FAILED"))?;85
if n == 0 {86
break;87
}88
if buffer.len() + n > 16384 {89
return Err(biometric_error("BIOMETRIC_FAILED"));90
}91
buffer.extend_from_slice(&chunk[..n]);92
}93
let status = child.wait().await.map_err(|_| biometric_error("BIOMETRIC_FAILED"))?;94
let value: Value = serde_json::from_slice(&buffer).map_err(|_| biometric_error("BIOMETRIC_FAILED"))?;95
if !status.success() || value.get("ok") != Some(&Value::Bool(true)) {96
let code = value.get("code").and_then(Value::as_str).filter(|c| c.starts_with("BIOMETRIC_") && c[10..].chars().all(|ch| ch.is_ascii_uppercase() || ch == '_')).unwrap_or("BIOMETRIC_FAILED");97
return Err(biometric_error(code));98
}99
Ok(value)100
};101
tokio::select! {102
result = tokio::time::timeout(timeout, work) => result.map_err(|_| biometric_error("BIOMETRIC_TIMEOUT"))?,103
_ = cancel.notified() => Err(biometric_error("BIOMETRIC_CANCELLED")),104
}105
}106
}108
pub struct BiometricService {109
file: PathBuf,110
vault: Arc<VaultStore>,111
runner: NativeRunner,112
info: ProviderInfo,113
epoch: AtomicU64,114
active: AtomicBool,115
cancel: Arc<Notify>,116
auto_armed: AtomicBool,117
auto_pending: tokio::sync::Mutex<()>,118
window_handle: Mutex<Option<String>>,119
require_auth: Box<dyn Fn() -> bool + Send + Sync>,120
reason: Box<dyn Fn() -> String + Send + Sync>,121
}123
struct ActiveGuard<'a>(&'a AtomicBool);124
impl Drop for ActiveGuard<'_> {125
fn drop(&mut self) {126
self.0.store(false, Ordering::SeqCst);127
}128
}130
impl BiometricService {131
pub fn new(132
storage_root: &Path,133
resources: &Path,134
vault: Arc<VaultStore>,135
require_auth: Box<dyn Fn() -> bool + Send + Sync>,136
reason: Box<dyn Fn() -> String + Send + Sync>,137
) -> Self {138
Self {139
file: storage_root.join("security").join("biometrics.json"),140
vault,141
runner: NativeRunner::for_resources(resources),142
info: provider_info(),143
epoch: AtomicU64::new(0),144
active: AtomicBool::new(false),145
cancel: Arc::new(Notify::new()),146
auto_armed: AtomicBool::new(true),147
auto_pending: tokio::sync::Mutex::new(()),148
window_handle: Mutex::new(None),149
require_auth,150
reason,151
}152
}154
/// Windows Hello needs the owner HWND of the main window (decimal string).155
pub fn set_window_handle(&self, hwnd: Option<String>) {156
*self.window_handle.lock() = hwnd;157
}159
pub fn is_active(&self) -> bool {160
self.active.load(Ordering::SeqCst)161
}163
pub fn arm_auto_unlock(&self) {164
self.auto_armed.store(true, Ordering::SeqCst);165
}167
pub fn disarm_auto_unlock(&self) {168
self.auto_armed.store(false, Ordering::SeqCst);169
}171
pub fn cancel(&self) {172
self.epoch.fetch_add(1, Ordering::SeqCst);173
self.cancel.notify_waiters();174
}176
fn metadata(&self) -> Option<Value> {177
let value: Value = serde_json::from_str(&std::fs::read_to_string(&self.file).ok()?).ok()?;178
let hex64 = |k: &str| value.get(k).and_then(Value::as_str).map(|s| s.len() == 64 && s.chars().all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))).unwrap_or(false);179
if value.get("schema").and_then(Value::as_u64) != Some(1) || !hex64("entryId") || !hex64("vaultId") {180
return None;181
}182
if let Some(wrapped) = value.get("wrapped") {183
if !wrapped.as_str().map(|w| w.len() <= 8192).unwrap_or(false) {184
return None;185
}186
}187
Some(value)188
}190
fn valid_metadata(&self) -> Option<Value> {191
let value = self.metadata()?;192
(value.get("vaultId").and_then(Value::as_str) == self.vault.instance_id().as_deref() && value.get("provider").and_then(Value::as_str) == Some(self.info.provider)).then_some(value)193
}195
async fn call(&self, op: &str, entry_id: Option<&str>, key: Option<&[u8]>, wrapped: Option<&str>) -> CoreResult<Value> {196
if self.info.provider == "none" {197
return Err(biometric_error("BIOMETRIC_UNAVAILABLE"));198
}199
if let Some(id) = entry_id {200
if id.len() != 64 || !id.chars().all(|c| c.is_ascii_hexdigit()) {201
return Err(biometric_error("BIOMETRIC_FAILED"));202
}203
}204
let mut hwnd = "0".to_string();205
if cfg!(windows) && op != "status" && op != "remove" {206
hwnd = self.window_handle.lock().clone().ok_or_else(|| biometric_error("BIOMETRIC_CANCELLED"))?;207
}208
let reason: String = (self.reason)().chars().take(200).collect();209
let request = json!({210
"op": op,211
"entryId": entry_id,212
"reason": reason,213
"key": key.map(|k| B64.encode(k)),214
"wrapped": wrapped,215
"hwnd": hwnd,216
"pid": std::process::id()217
});218
self.runner.run(&request, &self.cancel).await219
}221
pub async fn availability(&self) -> Value {222
match self.call("status", None, None, None).await {223
Ok(value) => {224
let available = value.get("available") == Some(&Value::Bool(true));225
json!({ "provider": self.info.provider, "protection": self.info.protection, "available": available, "code": value.get("code").cloned().filter(|c| !c.is_null()).unwrap_or_else(|| json!(if available { "BIOMETRIC_AVAILABLE" } else { "BIOMETRIC_UNAVAILABLE" })) })226
}227
Err(error) => json!({ "provider": self.info.provider, "protection": self.info.protection, "available": false, "code": error.code }),228
}229
}231
pub async fn status(&self) -> Value {232
let mut availability = self.availability().await;233
let enrolled = self.metadata().is_some();234
let valid = self.valid_metadata().is_some();235
availability["enrolled"] = json!(enrolled);236
availability["enabled"] = json!(valid);237
if enrolled && !valid {238
availability["code"] = json!("BIOMETRIC_ENROLLMENT_INVALIDATED");239
}240
availability241
}243
async fn run<T, F: std::future::Future<Output = CoreResult<T>>>(&self, work: impl FnOnce(u64) -> F) -> CoreResult<T> {244
if self.active.swap(true, Ordering::SeqCst) {245
return Err(biometric_error("BIOMETRIC_BUSY"));246
}247
let _guard = ActiveGuard(&self.active);248
let epoch = self.epoch.load(Ordering::SeqCst);249
match tokio::time::timeout(Duration::from_secs(70), work(epoch)).await {250
Ok(result) => result,251
Err(_) => {252
self.cancel();253
Err(biometric_error("BIOMETRIC_TIMEOUT"))254
}255
}256
}258
fn check(&self, epoch: u64) -> CoreResult<()> {259
if self.epoch.load(Ordering::SeqCst) != epoch {260
Err(biometric_error("BIOMETRIC_CANCELLED"))261
} else {262
Ok(())263
}264
}266
pub async fn enable(&self) -> CoreResult<Value> {267
self.vault.assert_unlocked()?;268
self.run(|epoch| async move {269
let availability = self.availability().await;270
self.check(epoch)?;271
if availability["available"] != true {272
return Err(biometric_error(availability["code"].as_str().unwrap_or("BIOMETRIC_UNAVAILABLE")));273
}274
let vault_id = self.vault.instance_id().ok_or_else(|| biometric_error("BIOMETRIC_ENROLLMENT_INVALIDATED"))?;275
let mut entry = [0u8; 32];276
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut entry);277
let entry_id = hex::encode(entry);278
let previous = self.metadata();279
self.call("authenticate", Some(&entry_id), None, None).await?;280
self.check(epoch)?;281
let key = self.vault.unlock_key_copy()?;282
let stored = self.call("store", Some(&entry_id), Some(key.as_ref()), None).await;283
drop(key);284
let result = (|| -> CoreResult<()> {285
let stored = stored?;286
self.check(epoch)?;287
if Some(vault_id.as_str()) != self.vault.instance_id().as_deref() {288
return Err(biometric_error("BIOMETRIC_ENROLLMENT_INVALIDATED"));289
}290
let mut record = json!({ "schema": 1, "vaultId": vault_id, "entryId": entry_id, "provider": self.info.provider });291
if let Some(wrapped) = stored.get("wrapped") {292
let ok = wrapped.as_str().map(|w| w.len() <= 8192 && !w.is_empty() && B64.decode(w).is_ok()).unwrap_or(false);293
if !ok {294
return Err(biometric_error("BIOMETRIC_CREDENTIAL_FAILED"));295
}296
record["wrapped"] = wrapped.clone();297
}298
atomic_write_json(&self.file, &record)299
})();300
if let Err(error) = result {301
let _ = self.call("remove", Some(&entry_id), None, None).await;302
return Err(error);303
}304
if let Some(previous) = previous {305
if previous["entryId"].as_str() != Some(entry_id.as_str()) {306
let _ = self.call("remove", previous["entryId"].as_str(), None, None).await;307
}308
}309
Ok(self.status().await)310
})311
.await312
}314
pub async fn disable(&self, reset: bool) -> CoreResult<Value> {315
if !reset {316
self.vault.assert_unlocked()?;317
}318
self.cancel();319
let previous = self.metadata();320
match std::fs::remove_file(&self.file) {321
Ok(()) => {}322
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}323
Err(e) => return Err(e.into()),324
}325
if let Some(previous) = previous {326
let _ = self.call("remove", previous["entryId"].as_str(), None, None).await;327
}328
Ok(json!({ "enabled": false, "enrolled": false }))329
}331
pub async fn unlock(&self) -> CoreResult<Value> {332
if self.vault.is_unlocked() {333
return Ok(serde_json::to_value(self.vault.status())?);334
}335
let generation = self.vault.generation();336
let meta = self.valid_metadata().ok_or_else(|| biometric_error("BIOMETRIC_MASTER_PASSWORD_REQUIRED"))?;337
let entry_id = meta["entryId"].as_str().unwrap_or("").to_string();338
self.run(|epoch| async move {339
let outcome = async {340
let result = self.call("retrieve", Some(&entry_id), None, meta.get("wrapped").and_then(Value::as_str)).await?;341
self.check(epoch)?;342
let encoded = Zeroizing::new(result.get("key").and_then(Value::as_str).unwrap_or("").to_string());343
if encoded.len() != 44 || !encoded.ends_with('=') {344
return Err(biometric_error("BIOMETRIC_INVALID_KEY"));345
}346
let key = Zeroizing::new(B64.decode(encoded.as_bytes()).map_err(|_| biometric_error("BIOMETRIC_INVALID_KEY"))?);347
if key.len() != 32 {348
return Err(biometric_error("BIOMETRIC_INVALID_KEY"));349
}350
if meta["vaultId"].as_str() != self.vault.instance_id().as_deref() || self.valid_metadata().and_then(|m| m["entryId"].as_str().map(str::to_string)) != Some(entry_id.clone()) {351
return Err(biometric_error("BIOMETRIC_ENROLLMENT_INVALIDATED"));352
}353
let status = self.vault.unlock_with_key_at_generation(&key, generation)?;354
self.disarm_auto_unlock();355
Ok(serde_json::to_value(status)?)356
}357
.await;358
if let Err(error) = &outcome {359
if error.code == "BIOMETRIC_ENROLLMENT_INVALIDATED" || error.code == "BIOMETRIC_INVALID_KEY" {360
let _ = std::fs::remove_file(&self.file);361
let _ = self.call("remove", Some(&entry_id), None, None).await;362
}363
}364
outcome365
})366
.await367
}369
/// Single-flight automatic prompt: armed at launch and after an automatic lock only.370
pub async fn auto_unlock(&self) -> Value {371
if self.vault.is_unlocked() {372
return json!({ "attempted": false, "reason": "UNLOCKED", "status": self.vault.status() });373
}374
let Ok(_pending) = self.auto_pending.try_lock() else {375
return json!({ "attempted": false, "reason": "PENDING" });376
};377
if !self.auto_armed.load(Ordering::SeqCst) {378
return json!({ "attempted": false, "reason": "NOT_ARMED" });379
}380
if self.valid_metadata().is_none() {381
return json!({ "attempted": false, "reason": "NOT_ENABLED" });382
}383
self.auto_armed.store(false, Ordering::SeqCst);384
let availability = self.availability().await;385
if availability["available"] != true {386
return json!({ "attempted": false, "reason": availability["code"] });387
}388
match self.unlock().await {389
Ok(status) => json!({ "attempted": true, "status": status }),390
Err(error) => json!({ "attempted": true, "error": { "code": error.code } }),391
}392
}394
pub async fn authenticate_sensitive_action(&self) -> CoreResult<Value> {395
self.vault.assert_unlocked()?;396
self.run(|epoch| async move {397
self.call("authenticate", None, None, None).await?;398
self.check(epoch)?;399
Ok(json!({ "authenticated": true }))400
})401
.await402
}404
pub async fn require_sensitive_action(&self) -> CoreResult<()> {405
if (self.require_auth)() {406
self.authenticate_sensitive_action().await?;407
}408
Ok(())409
}410
}412
#[cfg(test)]413
#[path = "../../tests/support/biometric_safety.rs"]414
mod safety_tests;SHA-256
d088cb96ce65c4b98ea1af0e0d34deb5712b62ff2903434126616946225c01a8CH-J Proprietary Software License 1.14 · Avots ir sniegts saskaņā ar CH-J Proprietary Software License 1.14. Tā pieejamība nemaina licences noteikumus un nepiešķir papildu tiesības.
Arhīvs SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f