use crate::error::AgentError;
use crate::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use windows::Security::Credentials::{PasswordCredential, PasswordVault};
use windows::core::HSTRING;
const MAX_CREDENTIAL_SIZE: usize = 2048;
const ALIAS_INDEX_FILE: &str = "auths-aliases.json";
pub struct WindowsCredentialStorage {
service_name: String,
alias_index_path: PathBuf,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(untagged)]
enum AliasEntry {
WithRole(String, String),
Legacy(String),
}
impl AliasEntry {
fn did(&self) -> &str {
match self {
AliasEntry::WithRole(did, _) | AliasEntry::Legacy(did) => did,
}
}
fn role(&self) -> KeyRole {
match self {
AliasEntry::WithRole(_, role_str) => {
role_str.parse::<KeyRole>().unwrap_or(KeyRole::Primary)
}
AliasEntry::Legacy(_) => KeyRole::Primary,
}
}
}
#[derive(serde::Serialize, serde::Deserialize, Default)]
struct AliasIndex {
aliases: HashMap<String, AliasEntry>,
}
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_types)]
impl WindowsCredentialStorage {
pub fn new(service_name: &str) -> Result<Self, AgentError> {
let _ = PasswordVault::new().map_err(|e| AgentError::BackendInitFailed {
backend: "windows-credential-manager",
error: format!("Failed to initialize PasswordVault: {}", e),
})?;
let data_dir = dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("auths");
if !data_dir.exists() {
fs::create_dir_all(&data_dir).map_err(|e| AgentError::BackendInitFailed {
backend: "windows-credential-manager",
error: format!("Failed to create data directory: {}", e),
})?;
}
let alias_index_path = data_dir.join(ALIAS_INDEX_FILE);
Ok(Self {
service_name: service_name.to_string(),
alias_index_path,
})
}
fn resource_name(&self, alias: &str) -> String {
format!("{}:{}", self.service_name, alias)
}
fn load_index(&self) -> AliasIndex {
if let Ok(data) = fs::read_to_string(&self.alias_index_path) {
serde_json::from_str(&data).unwrap_or_default()
} else {
AliasIndex::default()
}
}
fn save_index(&self, index: &AliasIndex) -> Result<(), AgentError> {
let data = serde_json::to_string_pretty(index).map_err(|e| {
AgentError::StorageError(format!("Failed to serialize alias index: {}", e))
})?;
fs::write(&self.alias_index_path, data)
.map_err(|e| AgentError::StorageError(format!("Failed to write alias index: {}", e)))?;
Ok(())
}
fn convert_error(e: windows::core::Error, context: &str) -> AgentError {
let code = e.code().0 as u32;
if code == 0x80070490 {
AgentError::KeyNotFound
} else {
AgentError::StorageError(format!("{}: {} (code: 0x{:08X})", context, e, code))
}
}
}
impl KeyStorage for WindowsCredentialStorage {
fn store_key(
&self,
alias: &KeyAlias,
identity_did: &IdentityDID,
role: KeyRole,
encrypted_key_data: &[u8],
) -> Result<(), AgentError> {
let alias = alias.as_str();
if encrypted_key_data.len() > MAX_CREDENTIAL_SIZE {
return Err(AgentError::CredentialTooLarge {
max_bytes: MAX_CREDENTIAL_SIZE,
actual_bytes: encrypted_key_data.len(),
});
}
let vault = PasswordVault::new().map_err(|e| AgentError::BackendInitFailed {
backend: "windows-credential-manager",
error: format!("Failed to initialize PasswordVault: {}", e),
})?;
let resource = self.resource_name(alias);
let resource_hstring = HSTRING::from(&resource);
let username_hstring = HSTRING::from(identity_did.as_str());
let password_hstring = HSTRING::from(BASE64.encode(encrypted_key_data));
if let Ok(existing) = vault.Retrieve(&resource_hstring, &username_hstring) {
let _ = vault.Remove(&existing);
}
let cred = PasswordCredential::CreatePasswordCredential(
&resource_hstring,
&username_hstring,
&password_hstring,
)
.map_err(|e| Self::convert_error(e, "Failed to create credential"))?;
vault
.Add(&cred)
.map_err(|e| Self::convert_error(e, "Failed to add credential"))?;
let mut index = self.load_index();
index.aliases.insert(
alias.to_string(),
AliasEntry::WithRole(identity_did.as_str().to_string(), role.to_string()),
);
self.save_index(&index)?;
Ok(())
}
fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
let alias = alias.as_str();
let vault = PasswordVault::new().map_err(|e| AgentError::BackendInitFailed {
backend: "windows-credential-manager",
error: format!("Failed to initialize PasswordVault: {}", e),
})?;
let index = self.load_index();
let entry = index.aliases.get(alias).ok_or(AgentError::KeyNotFound)?;
let identity_did = entry.did().to_string();
let role = entry.role();
let resource = self.resource_name(alias);
let resource_hstring = HSTRING::from(&resource);
let username_hstring = HSTRING::from(&identity_did);
let cred = vault
.Retrieve(&resource_hstring, &username_hstring)
.map_err(|e| Self::convert_error(e, "Failed to retrieve credential"))?;
cred.RetrievePassword()
.map_err(|e| Self::convert_error(e, "Failed to retrieve password"))?;
let password = cred
.Password()
.map_err(|e| Self::convert_error(e, "Failed to get password"))?
.to_string();
let key_data = BASE64
.decode(&password)
.map_err(|e| AgentError::StorageError(format!("Invalid key encoding: {}", e)))?;
Ok((IdentityDID::new_unchecked(identity_did), role, key_data))
}
fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
let alias = alias.as_str();
let vault = PasswordVault::new().map_err(|e| AgentError::BackendInitFailed {
backend: "windows-credential-manager",
error: format!("Failed to initialize PasswordVault: {}", e),
})?;
let mut index = self.load_index();
if let Some(entry) = index.aliases.get(alias).cloned() {
let identity_did = entry.did().to_string();
let resource = self.resource_name(alias);
let resource_hstring = HSTRING::from(&resource);
let username_hstring = HSTRING::from(&identity_did);
if let Ok(cred) = vault.Retrieve(&resource_hstring, &username_hstring) {
let _ = vault.Remove(&cred);
}
}
index.aliases.remove(alias);
self.save_index(&index)?;
Ok(())
}
fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
let index = self.load_index();
Ok(index
.aliases
.keys()
.map(|s| KeyAlias::new_unchecked(s.clone()))
.collect())
}
fn list_aliases_for_identity(
&self,
identity_did: &IdentityDID,
) -> Result<Vec<KeyAlias>, AgentError> {
let index = self.load_index();
Ok(index
.aliases
.iter()
.filter(|(_, entry)| entry.did() == identity_did.as_str())
.map(|(alias, _)| KeyAlias::new_unchecked(alias.clone()))
.collect())
}
fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
let alias = alias.as_str();
let index = self.load_index();
index
.aliases
.get(alias)
.map(|entry| IdentityDID::new_unchecked(entry.did().to_string()))
.ok_or(AgentError::KeyNotFound)
}
fn backend_name(&self) -> &'static str {
"windows-credential-manager"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_name() {
let storage = WindowsCredentialStorage {
service_name: "dev.auths.agent".to_string(),
alias_index_path: PathBuf::from("test.json"),
};
assert_eq!(storage.resource_name("my-key"), "dev.auths.agent:my-key");
}
#[test]
fn test_backend_name() {
let storage = WindowsCredentialStorage {
service_name: "test".to_string(),
alias_index_path: PathBuf::from("test.json"),
};
assert_eq!(storage.backend_name(), "windows-credential-manager");
}
#[test]
fn test_size_limit_constant() {
assert_eq!(MAX_CREDENTIAL_SIZE, 2048);
}
}