Skip to content

Commit 14e8a03

Browse files
committed
core/encrypted_key: add documentation
1 parent 2c8a3f0 commit 14e8a03

File tree

1 file changed

+182
-10
lines changed

1 file changed

+182
-10
lines changed

core/src/encrypted_key.rs

Lines changed: 182 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,61 @@
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+
159
use std::{fs, path::Path};
260

361
use 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)]
48132
pub 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

Comments
 (0)