use std::fs;
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use affinidi_tdk::dids::{DID, KeyType};
use serde::{Deserialize, Serialize};
use super::error::ProvisionError;
#[derive(Debug, Serialize, Deserialize)]
struct PersistedKey {
version: u8,
did: String,
private_key_multibase: String,
#[serde(default)]
note: String,
}
pub struct EphemeralSetupKey {
pub did: String,
private_key_mb: String,
}
impl EphemeralSetupKey {
pub fn generate() -> Result<Self, ProvisionError> {
let (did, secret) = DID::generate_did_key(KeyType::Ed25519)
.map_err(|e| ProvisionError::DidKeyGeneration(format!("generate did:key: {e}")))?;
let private_key_mb = secret.get_private_keymultibase().map_err(|e| {
ProvisionError::DidKeyGeneration(format!("get private key multibase: {e}"))
})?;
Ok(Self {
did,
private_key_mb,
})
}
pub fn private_key_multibase(&self) -> &str {
&self.private_key_mb
}
pub fn persist_to(&self, path: &Path) -> Result<(), ProvisionError> {
let payload = PersistedKey {
version: 1,
did: self.did.clone(),
private_key_multibase: self.private_key_mb.clone(),
note: "Ephemeral setup key for VTA integration provisioning. \
Delete once the integration is provisioned."
.into(),
};
let json = serde_json::to_string_pretty(&payload).map_err(ProvisionError::Serialization)?;
let mut opts = fs::OpenOptions::new();
opts.create(true).write(true).truncate(true);
#[cfg(unix)]
opts.mode(0o600);
let mut file = opts.open(path).map_err(|e| ProvisionError::SetupKeyIo {
path: path.to_path_buf(),
source: e,
})?;
use std::io::Write;
file.write_all(json.as_bytes())
.map_err(|e| ProvisionError::SetupKeyIo {
path: path.to_path_buf(),
source: e,
})?;
#[cfg(unix)]
{
let mut perm = fs::metadata(path)
.map_err(|e| ProvisionError::SetupKeyIo {
path: path.to_path_buf(),
source: e,
})?
.permissions();
perm.set_mode(0o600);
fs::set_permissions(path, perm).map_err(|e| ProvisionError::SetupKeyIo {
path: path.to_path_buf(),
source: e,
})?;
}
Ok(())
}
pub fn load_from(path: &Path) -> Result<Self, ProvisionError> {
let bytes = fs::read(path).map_err(|e| ProvisionError::SetupKeyIo {
path: path.to_path_buf(),
source: e,
})?;
let p: PersistedKey =
serde_json::from_slice(&bytes).map_err(|e| ProvisionError::SetupKeyParse {
path: path.to_path_buf(),
source: e,
})?;
if p.version != 1 {
return Err(ProvisionError::UnsupportedSetupKeyVersion {
path: path.to_path_buf(),
version: p.version,
});
}
Ok(Self {
did: p.did,
private_key_mb: p.private_key_multibase,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn generate_returns_did_key_with_private_multibase() {
let key = EphemeralSetupKey::generate().unwrap();
assert!(key.did.starts_with("did:key:z6Mk"));
assert!(!key.private_key_multibase().is_empty());
}
#[test]
fn two_generate_calls_produce_distinct_keys() {
let a = EphemeralSetupKey::generate().unwrap();
let b = EphemeralSetupKey::generate().unwrap();
assert_ne!(a.did, b.did);
}
#[test]
fn persist_roundtrip_preserves_did_and_key() {
let tmp = NamedTempFile::new().unwrap();
let original = EphemeralSetupKey::generate().unwrap();
original.persist_to(tmp.path()).unwrap();
let reloaded = EphemeralSetupKey::load_from(tmp.path()).unwrap();
assert_eq!(original.did, reloaded.did);
assert_eq!(
original.private_key_multibase(),
reloaded.private_key_multibase()
);
}
#[cfg(unix)]
#[test]
fn persisted_file_has_owner_only_permissions() {
use std::os::unix::fs::PermissionsExt;
let tmp = NamedTempFile::new().unwrap();
let key = EphemeralSetupKey::generate().unwrap();
key.persist_to(tmp.path()).unwrap();
let perm = fs::metadata(tmp.path()).unwrap().permissions();
assert_eq!(perm.mode() & 0o777, 0o600);
}
#[test]
fn load_rejects_unknown_version() {
let tmp = NamedTempFile::new().unwrap();
std::fs::write(
tmp.path(),
r#"{"version":42,"did":"did:key:z6Mk","private_key_multibase":"z"}"#,
)
.unwrap();
assert!(matches!(
EphemeralSetupKey::load_from(tmp.path()),
Err(ProvisionError::UnsupportedSetupKeyVersion { version: 42, .. })
));
}
}