use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::keys::Store as KeyStore;
use super::jws::{b64u, b64u_decode, AgentKey, Jwk};
use super::ViError;
const SEAL_CONTEXT_PREFIX: &str = "vi:p256:v1:";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredAgentKey {
pub kid: String,
pub created_at: String,
pub public_jwk: Jwk,
pub sealed_d: String,
#[serde(default)]
pub label: Option<String>,
}
pub fn vi_keys_dir(keys_dir: &Path) -> PathBuf {
keys_dir.join("vi")
}
fn key_path(keys_dir: &Path, kid: &str) -> PathBuf {
vi_keys_dir(keys_dir).join(format!("{kid}.json"))
}
pub fn save_agent_key(
keys_dir: &Path,
store: &KeyStore,
key: &AgentKey,
label: Option<&str>,
created_at: &str,
) -> Result<PathBuf, ViError> {
let dir = vi_keys_dir(keys_dir);
crate::fs_safe::create_dir_all_below(keys_dir, &dir)
.map_err(|e| ViError::Key(format!("create {}: {e}", dir.display())))?;
let path = key_path(keys_dir, &key.kid);
if path.exists() {
return Err(ViError::Key(format!(
"key {} already exists at {}",
key.kid,
path.display()
)));
}
let secret = key.secret_bytes();
let sealed = store
.encrypt_secret(&format!("{SEAL_CONTEXT_PREFIX}{}", key.kid), &secret)
.map_err(|e| ViError::Key(format!("seal: {e}")))?;
let rec = StoredAgentKey {
kid: key.kid.clone(),
created_at: created_at.into(),
public_jwk: key.public_jwk(),
sealed_d: b64u(&sealed),
label: label.map(str::to_string),
};
let json = serde_json::to_string_pretty(&rec).map_err(|e| ViError::Key(e.to_string()))?;
crate::fs_safe::write_atomic(&path, json.as_bytes(), 0o600)
.map_err(|e| ViError::Key(format!("write {}: {e}", path.display())))?;
Ok(path)
}
pub fn load_agent_key(keys_dir: &Path, store: &KeyStore, kid: &str) -> Result<AgentKey, ViError> {
let rec = read_stored(keys_dir, kid)?;
let sealed = b64u_decode(&rec.sealed_d)?;
let d = store
.decrypt_secret(&format!("{SEAL_CONTEXT_PREFIX}{kid}"), &sealed)
.map_err(|e| ViError::Key(format!("unseal {kid}: {e}")))?;
let key = AgentKey::from_secret(&d, kid)?;
if key.public_jwk().x != rec.public_jwk.x || key.public_jwk().y != rec.public_jwk.y {
return Err(ViError::Key(format!(
"stored public key for {kid} does not match its scalar"
)));
}
Ok(key)
}
pub fn read_stored(keys_dir: &Path, kid: &str) -> Result<StoredAgentKey, ViError> {
let path = key_path(keys_dir, kid);
let raw = std::fs::read_to_string(&path)
.map_err(|e| ViError::Key(format!("no VI key {kid} ({}): {e}", path.display())))?;
serde_json::from_str(&raw).map_err(|e| ViError::Key(format!("parse {}: {e}", path.display())))
}
pub fn list_agent_keys(keys_dir: &Path) -> Result<Vec<StoredAgentKey>, ViError> {
let dir = vi_keys_dir(keys_dir);
let Ok(rd) = std::fs::read_dir(&dir) else {
return Ok(Vec::new());
};
let mut out: Vec<StoredAgentKey> = rd
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|x| x == "json").unwrap_or(false))
.filter_map(|e| std::fs::read_to_string(e.path()).ok())
.filter_map(|s| serde_json::from_str(&s).ok())
.collect();
out.sort_by(|a, b| a.created_at.cmp(&b.created_at));
Ok(out)
}
pub fn default_agent_key(keys_dir: &Path) -> Result<StoredAgentKey, ViError> {
let all = list_agent_keys(keys_dir)?;
match all.len() {
0 => Err(ViError::Key("no VI key; run: treeship vi keygen".into())),
1 => Ok(all.into_iter().next().expect("one")),
n => Err(ViError::Key(format!("{n} VI keys; pass --key <kid>"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seal_unseal_round_trip_and_no_overwrite() {
let tmp = tempfile::tempdir().unwrap();
let ks_dir = tmp.path().join("keys");
let store = KeyStore::open(&ks_dir).unwrap();
let key = AgentKey::generate();
let p =
save_agent_key(&ks_dir, &store, &key, Some("test"), "2026-09-08T00:00:00Z").unwrap();
assert!(p.exists());
let back = load_agent_key(&ks_dir, &store, &key.kid).unwrap();
assert_eq!(back.public_jwk(), key.public_jwk());
assert!(save_agent_key(&ks_dir, &store, &key, None, "x").is_err());
assert_eq!(list_agent_keys(&ks_dir).unwrap().len(), 1);
assert_eq!(default_agent_key(&ks_dir).unwrap().kid, key.kid);
let raw = std::fs::read_to_string(&p).unwrap();
assert!(
!raw.contains(&b64u(&key.secret_bytes())),
"scalar must not be stored in the clear"
);
}
}