cli/commands/
misc.rs

1use libp2p_identity::PeerId;
2use node::{account::AccountSecretKey, p2p::identity::SecretKey};
3use std::{fs::File, io::Write};
4
5#[derive(Debug, clap::Args)]
6pub struct Misc {
7    #[command(subcommand)]
8    command: MiscCommand,
9}
10
11impl Misc {
12    pub fn run(self) -> anyhow::Result<()> {
13        match self.command {
14            MiscCommand::MinaEncryptedKey(command) => command.run(),
15            MiscCommand::MinaKeyPair(command) => command.run(),
16            MiscCommand::P2PKeyPair(command) => command.run(),
17        }
18    }
19}
20
21#[derive(Clone, Debug, clap::Subcommand)]
22pub enum MiscCommand {
23    MinaEncryptedKey(MinaEncryptedKey),
24    MinaKeyPair(MinaKeyPair),
25    P2PKeyPair(P2PKeyPair),
26}
27
28#[derive(Debug, Clone, clap::Args)]
29pub struct P2PKeyPair {
30    #[arg(long, short = 's', env = "OPENMINA_P2P_SEC_KEY")]
31    p2p_secret_key: Option<SecretKey>,
32}
33
34impl P2PKeyPair {
35    pub fn run(self) -> anyhow::Result<()> {
36        let secret_key = self.p2p_secret_key.unwrap_or_else(SecretKey::rand);
37        let public_key = secret_key.public_key();
38        let peer_id = public_key.peer_id();
39        let libp2p_peer_id = PeerId::try_from(peer_id)?;
40        println!("secret key: {secret_key}");
41        println!("public key: {public_key}");
42        println!("peer_id:    {peer_id}");
43        println!("libp2p_id:  {libp2p_peer_id}");
44
45        Ok(())
46    }
47}
48
49#[derive(Debug, Clone, clap::Args)]
50pub struct MinaKeyPair {
51    #[arg(long, short = 's', env = "OPENMINA_SEC_KEY")]
52    secret_key: Option<AccountSecretKey>,
53}
54
55impl MinaKeyPair {
56    pub fn run(self) -> anyhow::Result<()> {
57        let secret_key = self.secret_key.unwrap_or_else(AccountSecretKey::rand);
58        let public_key = secret_key.public_key();
59        println!("secret key: {secret_key}");
60        println!("public key: {public_key}");
61
62        Ok(())
63    }
64}
65
66/// Generate a new Mina key pair and save it as an encrypted JSON file
67///
68/// This command generates a new random Mina key pair (or uses a provided secret key)
69/// and saves it to an encrypted JSON file format compatible with key generation
70/// from the OCaml implementation.
71/// The encrypted file can be used as a producer key for block production.
72///
73/// This command replicates the secret box functionality from `src/lib/secret_box`
74/// in the OCaml implementation, providing compatible encrypted key storage.
75///
76/// # Examples
77///
78/// Generate a new encrypted key with password:
79/// ```bash
80/// openmina misc mina-encrypted-key --password mypassword --file producer-key
81/// ```
82///
83/// Generate a new encrypted key using environment variable for password:
84/// ```bash
85/// MINA_PRIVKEY_PASS=mypassword openmina misc mina-encrypted-key --file producer-key
86/// ```
87///
88/// Use an existing secret key:
89/// ```bash
90/// openmina misc mina-encrypted-key --secret-key EKE... --password mypassword
91/// ```
92#[derive(Debug, Clone, clap::Args)]
93pub struct MinaEncryptedKey {
94    /// Optional existing secret key to encrypt. If not provided, generates a
95    /// new random key
96    #[arg(long, short = 's', env = "OPENMINA_ENC_KEY")]
97    secret_key: Option<AccountSecretKey>,
98
99    /// Password to encrypt the key file with. Can be provided via
100    /// MINA_PRIVKEY_PASS environment variable
101    #[arg(env = "MINA_PRIVKEY_PASS", default_value = "")]
102    password: String,
103
104    /// Output file path for the encrypted key (default: mina_encrypted_key.json)
105    #[arg(long, short = 'f', default_value = "mina_encrypted_key.json")]
106    file: String,
107}
108
109impl MinaEncryptedKey {
110    /// Execute the mina-encrypted-key command
111    ///
112    /// Generates a new Mina key pair (or uses provided secret key) and saves it
113    /// as an encrypted JSON file that can be used for block production.
114    ///
115    /// It will also save the public key to the filename suffixed with `.pub`.
116    ///
117    /// # Returns
118    ///
119    /// * `Ok(())` - On successful key generation and file creation
120    /// * `Err(anyhow::Error)` - If key encryption or file writing fails
121    ///
122    /// # Output
123    ///
124    /// Prints the secret key and public key to stdout, and creates an encrypted
125    /// JSON file at the specified path.
126    pub fn run(self) -> anyhow::Result<()> {
127        let secret_key = self.secret_key.unwrap_or_else(AccountSecretKey::rand);
128        let public_key = secret_key.public_key();
129
130        // Save the public key to a separate file
131        let public_key_file = format!("{}.pub", self.file);
132
133        if File::open(&public_key_file).is_ok() {
134            return Err(anyhow::anyhow!(
135                "Public key file '{}' already exists. Please choose a different file name.",
136                public_key_file
137            ));
138        }
139
140        secret_key
141            .to_encrypted_file(&self.file, &self.password)
142            .map_err(|e| {
143                anyhow::anyhow!("Failed to encrypt key: {} into path '{}'", e, self.file,)
144            })?;
145        // Write the public key to the file
146        let mut public_key_file = File::create(public_key_file)
147            .map_err(|e| anyhow::anyhow!("Failed to create public key file: {}", e))?;
148        public_key_file
149            .write_all(public_key.to_string().as_bytes())
150            .map_err(|e| anyhow::anyhow!("Failed to write public key: {}", e))?;
151
152        println!("secret key: {secret_key}");
153        println!("public key: {public_key}");
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use std::fs;
162    use tempfile::TempDir;
163
164    #[test]
165    fn test_mina_encrypted_key_generates_random_key() {
166        let temp_dir = TempDir::new().unwrap();
167        let file_path = temp_dir.path().join("test_key.json");
168        let file_path_str = file_path.to_str().unwrap().to_string();
169
170        let cmd = MinaEncryptedKey {
171            secret_key: None,
172            password: "test_password".to_string(),
173            file: file_path_str.clone(),
174        };
175
176        let result = cmd.run();
177        assert!(result.is_ok());
178
179        // Verify the file was created
180        assert!(file_path.exists());
181
182        // Verify the file contains encrypted data (should be JSON)
183        let file_content = fs::read_to_string(&file_path).unwrap();
184        assert!(file_content.starts_with('{'));
185        assert!(file_content.ends_with('}'));
186    }
187
188    #[test]
189    fn test_mina_encrypted_key_with_provided_secret_key() {
190        let temp_dir = TempDir::new().unwrap();
191        let file_path = temp_dir.path().join("test_key_provided.json");
192        let file_path_str = file_path.to_str().unwrap().to_string();
193
194        let secret_key = AccountSecretKey::rand();
195        let expected_public_key = secret_key.public_key();
196
197        let cmd = MinaEncryptedKey {
198            secret_key: Some(secret_key),
199            password: "test_password".to_string(),
200            file: file_path_str.clone(),
201        };
202
203        let result = cmd.run();
204        assert!(result.is_ok());
205
206        // Verify the file was created
207        assert!(file_path.exists());
208
209        // Verify we can load the key back and it matches
210        let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "test_password");
211        assert!(loaded_key.is_ok());
212        let loaded_key = loaded_key.unwrap();
213        assert_eq!(loaded_key.public_key(), expected_public_key);
214    }
215
216    #[test]
217    fn test_mina_encrypted_key_with_empty_password() {
218        let temp_dir = TempDir::new().unwrap();
219        let file_path = temp_dir.path().join("test_key_no_pass.json");
220        let file_path_str = file_path.to_str().unwrap().to_string();
221
222        let cmd = MinaEncryptedKey {
223            secret_key: None,
224            password: "".to_string(),
225            file: file_path_str.clone(),
226        };
227
228        let result = cmd.run();
229        assert!(result.is_ok());
230
231        // Verify the file was created
232        assert!(file_path.exists());
233
234        // Verify we can load the key back with empty password
235        let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "");
236        assert!(loaded_key.is_ok());
237    }
238
239    #[test]
240    fn test_mina_encrypted_key_wrong_password_fails() {
241        let temp_dir = TempDir::new().unwrap();
242        let file_path = temp_dir.path().join("test_key_wrong_pass.json");
243        let file_path_str = file_path.to_str().unwrap().to_string();
244
245        let cmd = MinaEncryptedKey {
246            secret_key: None,
247            password: "correct_password".to_string(),
248            file: file_path_str.clone(),
249        };
250
251        let result = cmd.run();
252        assert!(result.is_ok());
253
254        // Verify loading with wrong password fails
255        let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "wrong_password");
256        assert!(loaded_key.is_err());
257    }
258
259    #[test]
260    fn test_mina_encrypted_key_invalid_file_path_fails() {
261        let cmd = MinaEncryptedKey {
262            secret_key: None,
263            password: "test_password".to_string(),
264            file: "/invalid/path/that/does/not/exist/key.json".to_string(),
265        };
266
267        let result = cmd.run();
268        assert!(result.is_err());
269    }
270
271    #[test]
272    fn test_mina_encrypted_key_roundtrip_compatibility() {
273        let temp_dir = TempDir::new().unwrap();
274        let file_path = temp_dir.path().join("test_roundtrip.json");
275        let file_path_str = file_path.to_str().unwrap().to_string();
276
277        // Generate a key with our command
278        let original_secret_key = AccountSecretKey::rand();
279        let original_public_key = original_secret_key.public_key();
280        let password = "roundtrip_test_password";
281
282        let cmd = MinaEncryptedKey {
283            secret_key: Some(original_secret_key.clone()),
284            password: password.to_string(),
285            file: file_path_str.clone(),
286        };
287
288        let result = cmd.run();
289        assert!(result.is_ok());
290
291        // Load the key back using the secret key methods directly
292        let loaded_secret_key = AccountSecretKey::from_encrypted_file(&file_path_str, password);
293        assert!(loaded_secret_key.is_ok());
294        let loaded_secret_key = loaded_secret_key.unwrap();
295        let loaded_public_key = loaded_secret_key.public_key();
296
297        // Verify the keys match exactly
298        assert_eq!(original_public_key, loaded_public_key);
299        assert_eq!(
300            original_secret_key.to_string(),
301            loaded_secret_key.to_string()
302        );
303    }
304
305    #[test]
306    fn test_mina_key_pair_generates_random_key() {
307        let cmd = MinaKeyPair { secret_key: None };
308
309        let result = cmd.run();
310        assert!(result.is_ok());
311    }
312
313    #[test]
314    fn test_mina_key_pair_with_provided_secret_key() {
315        let secret_key = AccountSecretKey::rand();
316        let cmd = MinaKeyPair {
317            secret_key: Some(secret_key),
318        };
319
320        let result = cmd.run();
321        assert!(result.is_ok());
322    }
323
324    #[test]
325    fn test_p2p_key_pair_generates_random_key() {
326        let cmd = P2PKeyPair {
327            p2p_secret_key: None,
328        };
329
330        let result = cmd.run();
331        assert!(result.is_ok());
332    }
333
334    #[test]
335    fn test_p2p_key_pair_with_provided_secret_key() {
336        let secret_key = SecretKey::rand();
337        let cmd = P2PKeyPair {
338            p2p_secret_key: Some(secret_key),
339        };
340
341        let result = cmd.run();
342        assert!(result.is_ok());
343    }
344}