@@ -10,16 +10,18 @@ pub struct Misc {
1010impl 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 ) ]
2021pub 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