iroh_node_util/
fs.rs

1//! Utilities for filesystem operations.
2
3use std::path::PathBuf;
4
5use anyhow::{bail, Context};
6use iroh::SecretKey;
7use tokio::io::AsyncWriteExt;
8
9/// Loads a [`SecretKey`] from the provided file, or stores a newly generated one
10/// at the given location.
11pub async fn load_secret_key(key_path: PathBuf) -> anyhow::Result<SecretKey> {
12    if key_path.exists() {
13        let keystr = tokio::fs::read(key_path).await?;
14
15        let ser_key = ssh_key::private::PrivateKey::from_openssh(keystr)?;
16        let ssh_key::private::KeypairData::Ed25519(kp) = ser_key.key_data() else {
17            bail!("invalid key format");
18        };
19        let secret_key = SecretKey::from_bytes(&kp.private.to_bytes());
20        Ok(secret_key)
21    } else {
22        let secret_key = SecretKey::generate(rand::rngs::OsRng);
23        let ckey = ssh_key::private::Ed25519Keypair {
24            public: secret_key.public().public().into(),
25            private: secret_key.secret().into(),
26        };
27        let ser_key =
28            ssh_key::private::PrivateKey::from(ckey).to_openssh(ssh_key::LineEnding::default())?;
29
30        // Try to canonicalize if possible
31        let key_path = key_path.canonicalize().unwrap_or(key_path);
32        let key_path_parent = key_path.parent().ok_or_else(|| {
33            anyhow::anyhow!("no parent directory found for '{}'", key_path.display())
34        })?;
35        tokio::fs::create_dir_all(&key_path_parent).await?;
36
37        // write to tempfile
38        let (file, temp_file_path) = tempfile::NamedTempFile::new_in(key_path_parent)
39            .context("unable to create tempfile")?
40            .into_parts();
41        let mut file = tokio::fs::File::from_std(file);
42        file.write_all(ser_key.as_bytes())
43            .await
44            .context("unable to write keyfile")?;
45        file.flush().await?;
46        drop(file);
47
48        // move file
49        tokio::fs::rename(temp_file_path, key_path)
50            .await
51            .context("failed to rename keyfile")?;
52
53        Ok(secret_key)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::load_secret_key;
60    #[tokio::test]
61    async fn secret_key_roundtrip() {
62        let dir = tempfile::tempdir().unwrap();
63        let path = dir.path().join("key");
64        let key1 = load_secret_key(path.clone()).await.unwrap();
65        let key2 = load_secret_key(path).await.unwrap();
66        assert_eq!(key1.to_bytes(), key2.to_bytes());
67    }
68}