1
+ //! # Encrypted Secret Key Implementation
2
+ //!
3
+ //! This module provides a unified interface for encrypting and decrypting
4
+ //! cryptographic secret keys used throughout the OpenMina node. It implements
5
+ //! password-based encryption compatible with the Mina Protocol's key format.
6
+ //!
7
+ //! ## Usage
8
+ //!
9
+ //! This module is used by:
10
+ //! - [`node::account::AccountSecretKey`] - Block producer keys for signing
11
+ //! blocks and transactions
12
+ //! - [`p2p::identity::SecretKey`] - P2P networking keys for node identity and
13
+ //! peer authentication
14
+ //!
15
+ //! ## Encryption Algorithms
16
+ //!
17
+ //! The implementation uses industry-standard cryptographic algorithms:
18
+ //!
19
+ //! ### Key Derivation
20
+ //! - **Argon2i**: Password-based key derivation function (PBKDF) with
21
+ //! configurable memory cost and time cost parameters
22
+ //! - **Default parameters**: 128MB memory cost, 6 iterations
23
+ //! - **Salt**: 32-byte random salt generated using OS entropy
24
+ //!
25
+ //! ### Symmetric Encryption
26
+ //! - **XSalsa20Poly1305**: Authenticated encryption with associated data (AEAD)
27
+ //! - **Key size**: 256-bit derived from password via Argon2i
28
+ //! - **Nonce**: 192-bit random nonce generated per encryption
29
+ //! - **Authentication**: Poly1305 MAC for ciphertext integrity
30
+ //!
31
+ //! ### Encoding
32
+ //! - **Base58**: All encrypted data (nonce, salt, ciphertext) encoded in
33
+ //! Base58 with version bytes for format compatibility with Mina Protocol
34
+ //! - **Version byte**: 2 for encryption data format compatibility
35
+ //!
36
+ //! ## File Format
37
+ //!
38
+ //! Encrypted keys are stored in JSON format with the following structure:
39
+ //! ```json
40
+ //! {
41
+ //! "box_primitive": "xsalsa20poly1305",
42
+ //! "pw_primitive": "argon2i",
43
+ //! "nonce": "base58-encoded-nonce",
44
+ //! "pwsalt": "base58-encoded-salt",
45
+ //! "pwdiff": [memory_cost_bytes, time_cost_iterations],
46
+ //! "ciphertext": "base58-encoded-encrypted-key"
47
+ //! }
48
+ //! ```
49
+ //!
50
+ //! This format ensures compatibility with existing Mina Protocol tooling and
51
+ //! wallet implementations.
52
+ //!
53
+ //! ## Reference Implementation
54
+ //!
55
+ //! The encryption format is based on the OCaml implementation in the Mina
56
+ //! repository:
57
+ //! [`src/lib/secret_box`](https://github.com/MinaProtocol/mina/tree/develop/src/lib/secret_box)
58
+
1
59
use std:: { fs, path:: Path } ;
2
60
3
61
use argon2:: { password_hash:: SaltString , Argon2 , PasswordHasher } ;
@@ -44,13 +102,56 @@ pub enum EncryptionError {
44
102
Other ( String ) ,
45
103
}
46
104
105
+ /// Represents the JSON structure of an encrypted secret key file.
106
+ ///
107
+ /// This structure defines the format used to store encrypted secret keys on
108
+ /// disk, compatible with the Mina Protocol's key file format. The file
109
+ /// contains all necessary cryptographic parameters for decryption.
110
+ ///
111
+ /// # JSON Format
112
+ /// When serialized, this structure produces a JSON file with the following
113
+ /// format:
114
+ /// ```json
115
+ /// {
116
+ /// "box_primitive": "xsalsa20poly1305",
117
+ /// "pw_primitive": "argon2i",
118
+ /// "nonce": "base58-encoded-nonce-with-version-byte",
119
+ /// "pwsalt": "base58-encoded-salt-with-version-byte",
120
+ /// "pwdiff": [memory_cost_in_bytes, time_cost_iterations],
121
+ /// "ciphertext": "base58-encoded-encrypted-key-with-version-byte"
122
+ /// }
123
+ /// ```
124
+ ///
125
+ /// # Security Considerations
126
+ /// - The `nonce` must be unique for each encryption operation
127
+ /// - The `pwsalt` should be cryptographically random
128
+ /// - The `pwdiff` parameters determine the computational cost of key
129
+ /// derivation
130
+ /// - All Base58-encoded fields include version bytes for format validation
47
131
#[ derive( Serialize , Deserialize , Debug ) ]
48
132
pub struct EncryptedSecretKeyFile {
133
+ /// Symmetric encryption algorithm identifier.
134
+ /// Always "xsalsa20poly1305" for compatibility.
49
135
box_primitive : String ,
136
+
137
+ /// Password-based key derivation function identifier.
138
+ /// Always "argon2i" for compatibility.
50
139
pw_primitive : String ,
140
+
141
+ /// Encryption nonce encoded in Base58 with version byte.
142
+ /// Used once per encryption to ensure semantic security.
51
143
nonce : Base58String ,
144
+
145
+ /// Argon2 salt encoded in Base58 with version byte.
146
+ /// Random value used in password-based key derivation.
52
147
pwsalt : Base58String ,
148
+
149
+ /// Argon2 parameters as (memory_cost_bytes, time_cost_iterations).
150
+ /// Determines computational difficulty of key derivation.
53
151
pwdiff : ( u32 , u32 ) ,
152
+
153
+ /// Encrypted secret key encoded in Base58 with version byte.
154
+ /// Contains the actual encrypted key data with authentication tag.
54
155
ciphertext : Base58String ,
55
156
}
56
157
@@ -80,12 +181,39 @@ pub trait EncryptedSecretKey {
80
181
const ENCRYPTION_DATA_VERSION_BYTE : u8 = 2 ;
81
182
const SECRET_KEY_PREFIX_BYTE : u8 = 1 ;
82
183
83
- // Based on the ocaml implementation
184
+ // Based on the OCaml implementation at:
185
+ // https://github.com/MinaProtocol/mina/tree/develop/src/lib/secret_box
84
186
const BOX_PRIMITIVE : & ' static str = "xsalsa20poly1305" ;
85
187
const PW_PRIMITIVE : & ' static str = "argon2i" ;
86
- // Note: Only used for enryption, for decryption use the pwdiff from the file
188
+ // Note: Only used for encryption, for decryption use the pwdiff from the
189
+ // file
87
190
const PW_DIFF : ( u32 , u32 ) = ( 134217728 , 6 ) ;
88
191
192
+ /// Decrypts an encrypted secret key file using the provided password.
193
+ ///
194
+ /// This method implements the decryption process compatible with Mina
195
+ /// Protocol's key format:
196
+ /// 1. Decodes Base58-encoded nonce, salt, and ciphertext from the file
197
+ /// 2. Derives encryption key from password using Argon2i with file's
198
+ /// parameters
199
+ /// 3. Decrypts the ciphertext using XSalsa20Poly1305 AEAD
200
+ /// 4. Returns the raw secret key bytes (with prefix byte stripped)
201
+ ///
202
+ /// # Parameters
203
+ /// - `encrypted`: The encrypted key file structure containing all
204
+ /// encryption metadata
205
+ /// - `password`: The password used to derive the decryption key
206
+ ///
207
+ /// # Returns
208
+ /// - `Ok(Vec<u8>)`: The raw secret key bytes on successful decryption
209
+ /// - `Err(EncryptionError)`: Various errors including wrong password,
210
+ /// corrupted data, or format incompatibility
211
+ ///
212
+ /// # Errors
213
+ /// - `EncryptionError::SecretBox`: AEAD decryption failure (wrong
214
+ /// password)
215
+ /// - `EncryptionError::Base58DecodeError`: Invalid Base58 encoding
216
+ /// - `EncryptionError::ArgonError`: Key derivation failure
89
217
fn try_decrypt (
90
218
encrypted : & EncryptedSecretKeyFile ,
91
219
password : & str ,
@@ -102,8 +230,9 @@ pub trait EncryptedSecretKey {
102
230
. ciphertext
103
231
. try_decode ( Self :: ENCRYPTION_DATA_VERSION_BYTE ) ?;
104
232
105
- // The argon crate's SaltString can only be built from base64 string, ocaml node encodes the salt in base58
106
- // So we decoded it from base58 first, then convert to base64 and lastly to SaltString
233
+ // The argon crate's SaltString can only be built from base64 string,
234
+ // but the OCaml Mina node encodes the salt in base58. So we decode it
235
+ // from base58 first, then convert to base64 and lastly to SaltString
107
236
let pwsalt_encoded = base64:: engine:: general_purpose:: STANDARD_NO_PAD . encode ( pwsalt) ;
108
237
let salt = SaltString :: from_b64 ( & pwsalt_encoded) ?;
109
238
@@ -121,10 +250,43 @@ pub trait EncryptedSecretKey {
121
250
// strip the prefix and create keypair
122
251
Ok ( decrypted)
123
252
}
124
- fn try_encrypt ( key : & [ u8 ] , password : & str ) -> Result < EncryptedSecretKeyFile , EncryptionError > {
253
+ /// Encrypts a secret key using password-based encryption.
254
+ ///
255
+ /// This method implements the encryption process compatible with Mina
256
+ /// Protocol's key format:
257
+ /// 1. Prefixes the key with a format version byte
258
+ /// 2. Generates a random salt and derives encryption key using Argon2i
259
+ /// 3. Encrypts the prefixed key using XSalsa20Poly1305 AEAD with a
260
+ /// random nonce
261
+ /// 4. Encodes all components (nonce, salt, ciphertext) in Base58 format
262
+ /// 5. Returns the complete encrypted file structure
263
+ ///
264
+ /// # Parameters
265
+ /// - `key`: The raw secret key bytes to encrypt
266
+ /// - `password`: The password used to derive the encryption key
267
+ ///
268
+ /// # Returns
269
+ /// - `Ok(EncryptedSecretKeyFile)`: Complete encrypted file structure
270
+ /// ready for JSON serialization
271
+ /// - `Err(EncryptionError)`: Encryption process failure
272
+ ///
273
+ /// # Errors
274
+ /// - `EncryptionError::ArgonError`: Key derivation failure
275
+ /// - `EncryptionError::SecretBox`: AEAD encryption failure
276
+ /// - `EncryptionError::HashMissing`: Argon2 hash generation failure
277
+ ///
278
+ /// # Security Notes
279
+ /// - Uses cryptographically secure random number generation for salt
280
+ /// and nonce
281
+ /// - Default Argon2i parameters: 128MB memory cost, 6 iterations
282
+ /// - Each encryption produces unique salt and nonce for security
283
+ fn try_encrypt (
284
+ key : & [ u8 ] ,
285
+ password : & str ,
286
+ ) -> Result < EncryptedSecretKeyFile , EncryptionError > {
125
287
let argon2 = setup_argon ( Self :: PW_DIFF ) ?;
126
288
127
- // add the prefix byt to the key
289
+ // add the prefix byte to the key
128
290
let mut key_prefixed = vec ! [ Self :: SECRET_KEY_PREFIX_BYTE ] ;
129
291
key_prefixed. extend ( key) ;
130
292
@@ -139,17 +301,27 @@ pub trait EncryptedSecretKey {
139
301
140
302
let ciphertext = cipher. encrypt ( & nonce, key_prefixed. as_slice ( ) ) ?;
141
303
142
- // Same reason as in decrypt, we ned to decode the SaltString from base64 then encode it to base58 bellow
304
+ // Same reason as in decrypt, we need to decode the SaltString from
305
+ // base64 then encode it to base58 below
143
306
let mut salt_bytes = [ 0 ; 32 ] ;
144
307
let salt_portion = salt. decode_b64 ( & mut salt_bytes) ?;
145
308
146
309
Ok ( EncryptedSecretKeyFile {
147
310
box_primitive : Self :: BOX_PRIMITIVE . to_string ( ) ,
148
311
pw_primitive : Self :: PW_PRIMITIVE . to_string ( ) ,
149
312
nonce : Base58String :: new ( & nonce, Self :: ENCRYPTION_DATA_VERSION_BYTE ) ,
150
- pwsalt : Base58String :: new ( salt_portion, Self :: ENCRYPTION_DATA_VERSION_BYTE ) ,
151
- pwdiff : ( argon2. params ( ) . m_cost ( ) * 1024 , argon2. params ( ) . t_cost ( ) ) ,
152
- ciphertext : Base58String :: new ( & ciphertext, Self :: ENCRYPTION_DATA_VERSION_BYTE ) ,
313
+ pwsalt : Base58String :: new (
314
+ salt_portion,
315
+ Self :: ENCRYPTION_DATA_VERSION_BYTE ,
316
+ ) ,
317
+ pwdiff : (
318
+ argon2. params ( ) . m_cost ( ) * 1024 ,
319
+ argon2. params ( ) . t_cost ( ) ,
320
+ ) ,
321
+ ciphertext : Base58String :: new (
322
+ & ciphertext,
323
+ Self :: ENCRYPTION_DATA_VERSION_BYTE ,
324
+ ) ,
153
325
} )
154
326
}
155
327
}
0 commit comments