openmina_core/
encrypted_key.rs1use std::{fs, path::Path};
2
3use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
4use base64::Engine;
5use crypto_secretbox::{
6 aead::{Aead, OsRng},
7 AeadCore, KeyInit, XSalsa20Poly1305,
8};
9use serde::{Deserialize, Serialize};
10
11#[derive(Serialize, Deserialize, Debug)]
12struct Base58String(String);
13
14impl Base58String {
15 pub fn new(raw: &[u8], version: u8) -> Self {
16 Base58String(bs58::encode(raw).with_check_version(version).into_string())
17 }
18
19 pub fn try_decode(&self, version: u8) -> Result<Vec<u8>, EncryptionError> {
20 let decoded = bs58::decode(&self.0).with_check(Some(version)).into_vec()?;
21 Ok(decoded[1..].to_vec())
22 }
23}
24
25#[derive(Debug, thiserror::Error)]
26pub enum EncryptionError {
27 #[error(transparent)]
28 SecretBox(#[from] crypto_secretbox::aead::Error),
29 #[error(transparent)]
30 ArgonError(#[from] argon2::Error),
31 #[error(transparent)]
32 PasswordHash(#[from] argon2::password_hash::Error),
33 #[error(transparent)]
34 Base58DecodeError(#[from] bs58::decode::Error),
35 #[error(transparent)]
36 CipherKeyInvalidLength(#[from] crypto_secretbox::cipher::InvalidLength),
37 #[error("Password hash missing after hash_password")]
38 HashMissing,
39 #[error(transparent)]
40 Io(#[from] std::io::Error),
41 #[error(transparent)]
42 SerdeJson(#[from] serde_json::Error),
43 #[error("Other: {0}")]
44 Other(String),
45}
46
47#[derive(Serialize, Deserialize, Debug)]
48pub struct EncryptedSecretKeyFile {
49 box_primitive: String,
50 pw_primitive: String,
51 nonce: Base58String,
52 pwsalt: Base58String,
53 pwdiff: (u32, u32),
54 ciphertext: Base58String,
55}
56
57impl EncryptedSecretKeyFile {
58 pub fn new(path: impl AsRef<Path>) -> Result<Self, EncryptionError> {
59 let file = fs::File::open(path)?;
60 Ok(serde_json::from_reader(file)?)
61 }
62}
63
64fn setup_argon(pwdiff: (u32, u32)) -> Result<Argon2<'static>, EncryptionError> {
65 let params = argon2::Params::new(
66 pwdiff.0 / 1024,
67 pwdiff.1,
68 argon2::Params::DEFAULT_P_COST,
69 None,
70 )?;
71
72 Ok(Argon2::new(
73 argon2::Algorithm::Argon2i,
74 Default::default(),
75 params,
76 ))
77}
78
79pub trait EncryptedSecretKey {
80 const ENCRYPTION_DATA_VERSION_BYTE: u8 = 2;
81 const SECRET_KEY_PREFIX_BYTE: u8 = 1;
82
83 const BOX_PRIMITIVE: &'static str = "xsalsa20poly1305";
85 const PW_PRIMITIVE: &'static str = "argon2i";
86 const PW_DIFF: (u32, u32) = (134217728, 6);
88
89 fn try_decrypt(
90 encrypted: &EncryptedSecretKeyFile,
91 password: &str,
92 ) -> Result<Vec<u8>, EncryptionError> {
93 let password = password.as_bytes();
95 let pwsalt = encrypted
96 .pwsalt
97 .try_decode(Self::ENCRYPTION_DATA_VERSION_BYTE)?;
98 let nonce = encrypted
99 .nonce
100 .try_decode(Self::ENCRYPTION_DATA_VERSION_BYTE)?;
101 let ciphertext = encrypted
102 .ciphertext
103 .try_decode(Self::ENCRYPTION_DATA_VERSION_BYTE)?;
104
105 let pwsalt_encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(pwsalt);
108 let salt = SaltString::from_b64(&pwsalt_encoded)?;
109
110 let argon2 = setup_argon(encrypted.pwdiff)?;
111 let password_hash = argon2
112 .hash_password(password, &salt)?
113 .hash
114 .ok_or(EncryptionError::HashMissing)?;
115 let password_bytes = password_hash.as_bytes();
116
117 let cipher = XSalsa20Poly1305::new_from_slice(password_bytes)?;
119 let decrypted = cipher.decrypt(nonce.as_slice().into(), ciphertext.as_ref())?;
120
121 Ok(decrypted)
123 }
124 fn try_encrypt(key: &[u8], password: &str) -> Result<EncryptedSecretKeyFile, EncryptionError> {
125 let argon2 = setup_argon(Self::PW_DIFF)?;
126
127 let mut key_prefixed = vec![Self::SECRET_KEY_PREFIX_BYTE];
129 key_prefixed.extend(key);
130
131 let salt = SaltString::generate(&mut OsRng);
132 let password_hash = argon2
133 .hash_password(password.as_bytes(), &salt)?
134 .hash
135 .ok_or(EncryptionError::HashMissing)?;
136
137 let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng);
138 let cipher = XSalsa20Poly1305::new_from_slice(password_hash.as_bytes())?;
139
140 let ciphertext = cipher.encrypt(&nonce, key_prefixed.as_slice())?;
141
142 let mut salt_bytes = [0; 32];
144 let salt_portion = salt.decode_b64(&mut salt_bytes)?;
145
146 Ok(EncryptedSecretKeyFile {
147 box_primitive: Self::BOX_PRIMITIVE.to_string(),
148 pw_primitive: Self::PW_PRIMITIVE.to_string(),
149 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),
153 })
154 }
155}