release: OpenNexus 0.5.0

This commit is contained in:
2026-09-17 20:59:32 +08:00
parent e86809b238
commit f7d441bd92
34 changed files with 509 additions and 60 deletions
+74
View File
@@ -354,6 +354,52 @@ pub struct CredentialBroker {
}
impl CredentialBroker {
#[cfg(windows)]
fn auto_unlock_path(&self) -> Result<PathBuf> {
Ok(self
.path
.parent()
.ok_or("CREDENTIAL_PATH_INVALID")?
.join("auto-unlock.dpapi"))
}
/// Unlocks with a random secret protected by Windows DPAPI. A new vault is initialized
/// automatically; an existing password vault is never overwritten implicitly.
#[cfg(windows)]
pub fn ensure_system_unlock(&mut self) -> Result<bool> {
let key_path = self.auto_unlock_path()?;
if let Some(secret) = crate::credential_autounlock::load(&key_path)? {
self.unlock(secret)?;
return Ok(true);
}
if self.path.exists() {
return Ok(false);
}
let mut secret = Zeroizing::new(vec![0u8; 32]);
rand::rngs::OsRng
.try_fill_bytes(&mut secret)
.map_err(|_| "CREDENTIAL_ENTROPY_FAILED")?;
crate::credential_autounlock::save(&key_path, &secret)?;
self.unlock(secret)?;
Ok(true)
}
#[cfg(windows)]
pub fn enable_system_unlock(&self, password: &[u8]) -> Result<()> {
self.session()?;
crate::credential_autounlock::save(&self.auto_unlock_path()?, password)
}
#[cfg(windows)]
pub fn has_system_unlock(&self) -> bool {
self.auto_unlock_path().is_ok_and(|path| path.is_file())
}
#[cfg(not(windows))]
pub fn has_system_unlock(&self) -> bool {
false
}
/// 源来自本机文件选择器,而不是原始 WebView 路径。导入是幂等的;冲突的 ID 会停止整个事务。
pub fn import_fernet(
&mut self,
@@ -1041,6 +1087,34 @@ mod tests {
fn password() -> Zeroizing<Vec<u8>> {
Zeroizing::new(b"test-only-password-123".to_vec())
}
#[cfg(windows)]
#[test]
fn system_unlock_survives_broker_restart() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("credentials/stronghold.v1");
let id = CredentialId {
scope: Scope::Provider,
id: "provider-restart".into(),
};
{
let mut broker = CredentialBroker::new(path.clone());
assert!(broker.ensure_system_unlock().unwrap());
broker
.put(&id, Zeroizing::new(b"restart-secret".to_vec()))
.unwrap();
}
let mut restarted = CredentialBroker::new(path);
assert!(restarted.ensure_system_unlock().unwrap());
assert_eq!(
restarted
.resolve(&Scope::Provider, &id)
.unwrap()
.unwrap()
.as_slice(),
b"restart-secret"
);
}
fn b04_fixture() -> (Vec<u8>, String, BTreeMap<String, String>) {
let fixture: serde_json::Value =
serde_json::from_str(include_str!("../tests/fixtures/fernet-python.json")).unwrap();