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+
159use std:: { fs, path:: Path } ;
260
361use argon2:: { password_hash:: SaltString , Argon2 , PasswordHasher } ;
@@ -44,13 +102,56 @@ pub enum EncryptionError {
44102 Other ( String ) ,
45103}
46104
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
47131#[ derive( Serialize , Deserialize , Debug ) ]
48132pub struct EncryptedSecretKeyFile {
133+ /// Symmetric encryption algorithm identifier.
134+ /// Always "xsalsa20poly1305" for compatibility.
49135 box_primitive : String ,
136+
137+ /// Password-based key derivation function identifier.
138+ /// Always "argon2i" for compatibility.
50139 pw_primitive : String ,
140+
141+ /// Encryption nonce encoded in Base58 with version byte.
142+ /// Used once per encryption to ensure semantic security.
51143 nonce : Base58String ,
144+
145+ /// Argon2 salt encoded in Base58 with version byte.
146+ /// Random value used in password-based key derivation.
52147 pwsalt : Base58String ,
148+
149+ /// Argon2 parameters as (memory_cost_bytes, time_cost_iterations).
150+ /// Determines computational difficulty of key derivation.
53151 pwdiff : ( u32 , u32 ) ,
152+
153+ /// Encrypted secret key encoded in Base58 with version byte.
154+ /// Contains the actual encrypted key data with authentication tag.
54155 ciphertext : Base58String ,
55156}
56157
@@ -80,12 +181,39 @@ pub trait EncryptedSecretKey {
80181 const ENCRYPTION_DATA_VERSION_BYTE : u8 = 2 ;
81182 const SECRET_KEY_PREFIX_BYTE : u8 = 1 ;
82183
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
84186 const BOX_PRIMITIVE : & ' static str = "xsalsa20poly1305" ;
85187 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
87190 const PW_DIFF : ( u32 , u32 ) = ( 134217728 , 6 ) ;
88191
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
89217 fn try_decrypt (
90218 encrypted : & EncryptedSecretKeyFile ,
91219 password : & str ,
@@ -102,8 +230,9 @@ pub trait EncryptedSecretKey {
102230 . ciphertext
103231 . try_decode ( Self :: ENCRYPTION_DATA_VERSION_BYTE ) ?;
104232
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
107236 let pwsalt_encoded = base64:: engine:: general_purpose:: STANDARD_NO_PAD . encode ( pwsalt) ;
108237 let salt = SaltString :: from_b64 ( & pwsalt_encoded) ?;
109238
@@ -121,10 +250,43 @@ pub trait EncryptedSecretKey {
121250 // strip the prefix and create keypair
122251 Ok ( decrypted)
123252 }
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 > {
125287 let argon2 = setup_argon ( Self :: PW_DIFF ) ?;
126288
127- // add the prefix byt to the key
289+ // add the prefix byte to the key
128290 let mut key_prefixed = vec ! [ Self :: SECRET_KEY_PREFIX_BYTE ] ;
129291 key_prefixed. extend ( key) ;
130292
@@ -139,17 +301,27 @@ pub trait EncryptedSecretKey {
139301
140302 let ciphertext = cipher. encrypt ( & nonce, key_prefixed. as_slice ( ) ) ?;
141303
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
143306 let mut salt_bytes = [ 0 ; 32 ] ;
144307 let salt_portion = salt. decode_b64 ( & mut salt_bytes) ?;
145308
146309 Ok ( EncryptedSecretKeyFile {
147310 box_primitive : Self :: BOX_PRIMITIVE . to_string ( ) ,
148311 pw_primitive : Self :: PW_PRIMITIVE . to_string ( ) ,
149312 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+ ) ,
153325 } )
154326 }
155327}
0 commit comments