@@ -10,16 +10,18 @@ pub struct Misc {
10
10
impl Misc {
11
11
pub fn run ( self ) -> anyhow:: Result < ( ) > {
12
12
match self . command {
13
- MiscCommand :: P2PKeyPair ( command) => command. run ( ) ,
13
+ MiscCommand :: MinaEncryptedKey ( command) => command. run ( ) ,
14
14
MiscCommand :: MinaKeyPair ( command) => command. run ( ) ,
15
+ MiscCommand :: P2PKeyPair ( command) => command. run ( ) ,
15
16
}
16
17
}
17
18
}
18
19
19
20
#[ derive( Clone , Debug , clap:: Subcommand ) ]
20
21
pub enum MiscCommand {
21
- P2PKeyPair ( P2PKeyPair ) ,
22
+ MinaEncryptedKey ( MinaEncryptedKey ) ,
22
23
MinaKeyPair ( MinaKeyPair ) ,
24
+ P2PKeyPair ( P2PKeyPair ) ,
23
25
}
24
26
25
27
#[ derive( Debug , Clone , clap:: Args ) ]
@@ -59,3 +61,260 @@ impl MinaKeyPair {
59
61
Ok ( ( ) )
60
62
}
61
63
}
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