Skip to content

Commit b7947ea

Browse files
committed
CLI: add a command to generate an encrypted key file
Mimicks secret_box in the Caml node
1 parent d48fdfc commit b7947ea

File tree

1 file changed

+261
-2
lines changed

1 file changed

+261
-2
lines changed

cli/src/commands/misc.rs

Lines changed: 261 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ pub struct Misc {
1010
impl Misc {
1111
pub fn run(self) -> anyhow::Result<()> {
1212
match self.command {
13-
MiscCommand::P2PKeyPair(command) => command.run(),
13+
MiscCommand::MinaEncryptedKey(command) => command.run(),
1414
MiscCommand::MinaKeyPair(command) => command.run(),
15+
MiscCommand::P2PKeyPair(command) => command.run(),
1516
}
1617
}
1718
}
1819

1920
#[derive(Clone, Debug, clap::Subcommand)]
2021
pub enum MiscCommand {
21-
P2PKeyPair(P2PKeyPair),
22+
MinaEncryptedKey(MinaEncryptedKey),
2223
MinaKeyPair(MinaKeyPair),
24+
P2PKeyPair(P2PKeyPair),
2325
}
2426

2527
#[derive(Debug, Clone, clap::Args)]
@@ -59,3 +61,260 @@ impl MinaKeyPair {
5961
Ok(())
6062
}
6163
}
64+
65+
/// Generate a new Mina key pair and save it as an encrypted JSON file
66+
///
67+
/// This command generates a new random Mina key pair (or uses a provided secret key)
68+
/// and saves it to an encrypted JSON file format compatible with the Mina protocol.
69+
/// The encrypted file can be used as a producer key for block production.
70+
///
71+
/// This command replicates the secret box functionality from `src/lib/secret_box`
72+
/// in the OCaml implementation, providing compatible encrypted key storage.
73+
///
74+
/// # Examples
75+
///
76+
/// Generate a new encrypted key with password:
77+
/// ```bash
78+
/// openmina misc mina-encrypted-key --password mypassword --file producer-key
79+
/// ```
80+
///
81+
/// Generate a new encrypted key using environment variable for password:
82+
/// ```bash
83+
/// MINA_PRIVKEY_PASS=mypassword openmina misc mina-encrypted-key --file producer-key
84+
/// ```
85+
///
86+
/// Use an existing secret key:
87+
/// ```bash
88+
/// openmina misc mina-encrypted-key --secret-key EKE... --password mypassword
89+
/// ```
90+
#[derive(Debug, Clone, clap::Args)]
91+
pub struct MinaEncryptedKey {
92+
/// Optional existing secret key to encrypt. If not provided, generates a new random key
93+
#[arg(long, short = 's', env = "OPENMINA_ENC_KEY")]
94+
secret_key: Option<AccountSecretKey>,
95+
96+
/// Password to encrypt the key file with. Can be provided via MINA_PRIVKEY_PASS environment variable
97+
#[arg(env = "MINA_PRIVKEY_PASS", default_value = "")]
98+
password: String,
99+
100+
/// Output file path for the encrypted key (default: mina_encrypted_key.json)
101+
#[arg(long, short = 'f', default_value = "mina_encrypted_key.json")]
102+
file: String,
103+
}
104+
105+
impl MinaEncryptedKey {
106+
/// Execute the mina-encrypted-key command
107+
///
108+
/// Generates a new Mina key pair (or uses provided secret key) and saves it
109+
/// as an encrypted JSON file that can be used for block production.
110+
///
111+
/// # Returns
112+
///
113+
/// * `Ok(())` - On successful key generation and file creation
114+
/// * `Err(anyhow::Error)` - If key encryption or file writing fails
115+
///
116+
/// # Output
117+
///
118+
/// Prints the secret key and public key to stdout, and creates an encrypted
119+
/// JSON file at the specified path.
120+
pub fn run(self) -> anyhow::Result<()> {
121+
let secret_key = self.secret_key.unwrap_or_else(AccountSecretKey::rand);
122+
secret_key
123+
.to_encrypted_file(&self.file, &self.password)
124+
.map_err(|e| {
125+
anyhow::anyhow!("Failed to encrypt key: {} into path '{}'", e, self.file,)
126+
})?;
127+
let public_key = secret_key.public_key();
128+
println!("secret key: {secret_key}");
129+
println!("public key: {public_key}");
130+
Ok(())
131+
}
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use super::*;
137+
use std::fs;
138+
use tempfile::TempDir;
139+
140+
#[test]
141+
fn test_mina_encrypted_key_generates_random_key() {
142+
let temp_dir = TempDir::new().unwrap();
143+
let file_path = temp_dir.path().join("test_key.json");
144+
let file_path_str = file_path.to_str().unwrap().to_string();
145+
146+
let cmd = MinaEncryptedKey {
147+
secret_key: None,
148+
password: "test_password".to_string(),
149+
file: file_path_str.clone(),
150+
};
151+
152+
let result = cmd.run();
153+
assert!(result.is_ok());
154+
155+
// Verify the file was created
156+
assert!(file_path.exists());
157+
158+
// Verify the file contains encrypted data (should be JSON)
159+
let file_content = fs::read_to_string(&file_path).unwrap();
160+
assert!(file_content.starts_with('{'));
161+
assert!(file_content.ends_with('}'));
162+
}
163+
164+
#[test]
165+
fn test_mina_encrypted_key_with_provided_secret_key() {
166+
let temp_dir = TempDir::new().unwrap();
167+
let file_path = temp_dir.path().join("test_key_provided.json");
168+
let file_path_str = file_path.to_str().unwrap().to_string();
169+
170+
let secret_key = AccountSecretKey::rand();
171+
let expected_public_key = secret_key.public_key();
172+
173+
let cmd = MinaEncryptedKey {
174+
secret_key: Some(secret_key),
175+
password: "test_password".to_string(),
176+
file: file_path_str.clone(),
177+
};
178+
179+
let result = cmd.run();
180+
assert!(result.is_ok());
181+
182+
// Verify the file was created
183+
assert!(file_path.exists());
184+
185+
// Verify we can load the key back and it matches
186+
let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "test_password");
187+
assert!(loaded_key.is_ok());
188+
let loaded_key = loaded_key.unwrap();
189+
assert_eq!(loaded_key.public_key(), expected_public_key);
190+
}
191+
192+
#[test]
193+
fn test_mina_encrypted_key_with_empty_password() {
194+
let temp_dir = TempDir::new().unwrap();
195+
let file_path = temp_dir.path().join("test_key_no_pass.json");
196+
let file_path_str = file_path.to_str().unwrap().to_string();
197+
198+
let cmd = MinaEncryptedKey {
199+
secret_key: None,
200+
password: "".to_string(),
201+
file: file_path_str.clone(),
202+
};
203+
204+
let result = cmd.run();
205+
assert!(result.is_ok());
206+
207+
// Verify the file was created
208+
assert!(file_path.exists());
209+
210+
// Verify we can load the key back with empty password
211+
let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "");
212+
assert!(loaded_key.is_ok());
213+
}
214+
215+
#[test]
216+
fn test_mina_encrypted_key_wrong_password_fails() {
217+
let temp_dir = TempDir::new().unwrap();
218+
let file_path = temp_dir.path().join("test_key_wrong_pass.json");
219+
let file_path_str = file_path.to_str().unwrap().to_string();
220+
221+
let cmd = MinaEncryptedKey {
222+
secret_key: None,
223+
password: "correct_password".to_string(),
224+
file: file_path_str.clone(),
225+
};
226+
227+
let result = cmd.run();
228+
assert!(result.is_ok());
229+
230+
// Verify loading with wrong password fails
231+
let loaded_key = AccountSecretKey::from_encrypted_file(&file_path_str, "wrong_password");
232+
assert!(loaded_key.is_err());
233+
}
234+
235+
#[test]
236+
fn test_mina_encrypted_key_invalid_file_path_fails() {
237+
let cmd = MinaEncryptedKey {
238+
secret_key: None,
239+
password: "test_password".to_string(),
240+
file: "/invalid/path/that/does/not/exist/key.json".to_string(),
241+
};
242+
243+
let result = cmd.run();
244+
assert!(result.is_err());
245+
}
246+
247+
#[test]
248+
fn test_mina_encrypted_key_roundtrip_compatibility() {
249+
let temp_dir = TempDir::new().unwrap();
250+
let file_path = temp_dir.path().join("test_roundtrip.json");
251+
let file_path_str = file_path.to_str().unwrap().to_string();
252+
253+
// Generate a key with our command
254+
let original_secret_key = AccountSecretKey::rand();
255+
let original_public_key = original_secret_key.public_key();
256+
let password = "roundtrip_test_password";
257+
258+
let cmd = MinaEncryptedKey {
259+
secret_key: Some(original_secret_key.clone()),
260+
password: password.to_string(),
261+
file: file_path_str.clone(),
262+
};
263+
264+
let result = cmd.run();
265+
assert!(result.is_ok());
266+
267+
// Load the key back using the secret key methods directly
268+
let loaded_secret_key = AccountSecretKey::from_encrypted_file(&file_path_str, password);
269+
assert!(loaded_secret_key.is_ok());
270+
let loaded_secret_key = loaded_secret_key.unwrap();
271+
let loaded_public_key = loaded_secret_key.public_key();
272+
273+
// Verify the keys match exactly
274+
assert_eq!(original_public_key, loaded_public_key);
275+
assert_eq!(
276+
original_secret_key.to_string(),
277+
loaded_secret_key.to_string()
278+
);
279+
}
280+
281+
#[test]
282+
fn test_mina_key_pair_generates_random_key() {
283+
let cmd = MinaKeyPair { secret_key: None };
284+
285+
let result = cmd.run();
286+
assert!(result.is_ok());
287+
}
288+
289+
#[test]
290+
fn test_mina_key_pair_with_provided_secret_key() {
291+
let secret_key = AccountSecretKey::rand();
292+
let cmd = MinaKeyPair {
293+
secret_key: Some(secret_key),
294+
};
295+
296+
let result = cmd.run();
297+
assert!(result.is_ok());
298+
}
299+
300+
#[test]
301+
fn test_p2p_key_pair_generates_random_key() {
302+
let cmd = P2PKeyPair {
303+
p2p_secret_key: None,
304+
};
305+
306+
let result = cmd.run();
307+
assert!(result.is_ok());
308+
}
309+
310+
#[test]
311+
fn test_p2p_key_pair_with_provided_secret_key() {
312+
let secret_key = SecretKey::rand();
313+
let cmd = P2PKeyPair {
314+
p2p_secret_key: Some(secret_key),
315+
};
316+
317+
let result = cmd.run();
318+
assert!(result.is_ok());
319+
}
320+
}

0 commit comments

Comments
 (0)