1use crate::error::SignerError;
10use aes::cipher::{KeyIvInit, StreamCipher};
11use core::fmt;
12use zeroize::Zeroizing;
13
14type Aes128Ctr = ctr::Ctr64BE<aes::Aes128>;
16
17#[derive(Debug, Clone)]
19pub struct ScryptParams {
20 pub n: u32,
22 pub r: u32,
24 pub p: u32,
26 pub dklen: u32,
28}
29
30impl Default for ScryptParams {
31 fn default() -> Self {
33 Self {
34 n: 262144, r: 8,
36 p: 1,
37 dklen: 32,
38 }
39 }
40}
41
42impl ScryptParams {
43 #[must_use]
45 pub fn light() -> Self {
46 Self {
47 n: 4096, r: 8,
49 p: 6,
50 dklen: 32,
51 }
52 }
53}
54
55#[derive(Clone)]
59pub struct Keystore {
60 pub id: String,
62 pub address: String,
64 scrypt_params: ScryptParams,
66 salt: [u8; 32],
68 iv: [u8; 16],
70 ciphertext: Vec<u8>,
72 mac: [u8; 32],
74}
75
76impl Keystore {
77 pub fn encrypt(
84 private_key: &[u8],
85 password: &[u8],
86 params: &ScryptParams,
87 ) -> Result<Self, SignerError> {
88 if private_key.len() != 32 {
89 return Err(SignerError::InvalidPrivateKey(
90 "key must be 32 bytes".into(),
91 ));
92 }
93
94 let mut salt = [0u8; 32];
96 crate::security::secure_random(&mut salt)?;
97 let mut iv = [0u8; 16];
98 crate::security::secure_random(&mut iv)?;
99
100 let derived = derive_scrypt_key(password, &salt, params)?;
102
103 let mut ciphertext = private_key.to_vec();
105 let mut cipher = Aes128Ctr::new(derived[..16].into(), iv.as_ref().into());
106 cipher.apply_keystream(&mut ciphertext);
107
108 let mut mac_input = Vec::with_capacity(16 + ciphertext.len());
110 mac_input.extend_from_slice(&derived[16..32]);
111 mac_input.extend_from_slice(&ciphertext);
112 let mac = keccak256(&mac_input);
113
114 use crate::traits::KeyPair;
116 let signer = super::EthereumSigner::from_bytes(private_key)?;
117 let address = signer.address_checksum();
118
119 let mut uuid_bytes = [0u8; 16];
121 crate::security::secure_random(&mut uuid_bytes)?;
122 uuid_bytes[6] = (uuid_bytes[6] & 0x0F) | 0x40;
124 uuid_bytes[8] = (uuid_bytes[8] & 0x3F) | 0x80;
125 let id = format!(
126 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
127 u32::from_be_bytes([uuid_bytes[0], uuid_bytes[1], uuid_bytes[2], uuid_bytes[3]]),
128 u16::from_be_bytes([uuid_bytes[4], uuid_bytes[5]]),
129 u16::from_be_bytes([uuid_bytes[6], uuid_bytes[7]]),
130 u16::from_be_bytes([uuid_bytes[8], uuid_bytes[9]]),
131 u64::from_be_bytes([
132 0,
133 0,
134 uuid_bytes[10],
135 uuid_bytes[11],
136 uuid_bytes[12],
137 uuid_bytes[13],
138 uuid_bytes[14],
139 uuid_bytes[15]
140 ]),
141 );
142
143 Ok(Self {
144 id,
145 address,
146 scrypt_params: params.clone(),
147 salt,
148 iv,
149 ciphertext,
150 mac,
151 })
152 }
153
154 pub fn decrypt(&self, password: &[u8]) -> Result<Zeroizing<Vec<u8>>, SignerError> {
158 let derived = derive_scrypt_key(password, &self.salt, &self.scrypt_params)?;
160
161 let mut mac_input = Vec::with_capacity(16 + self.ciphertext.len());
163 mac_input.extend_from_slice(&derived[16..32]);
164 mac_input.extend_from_slice(&self.ciphertext);
165 let computed_mac = keccak256(&mac_input);
166
167 use subtle::ConstantTimeEq;
168 if computed_mac.ct_eq(&self.mac).unwrap_u8() != 1 {
169 return Err(SignerError::InvalidSignature(
170 "keystore MAC verification failed (wrong password?)".into(),
171 ));
172 }
173
174 let mut plaintext = self.ciphertext.clone();
176 let mut cipher = Aes128Ctr::new(derived[..16].into(), self.iv.as_ref().into());
177 cipher.apply_keystream(&mut plaintext);
178
179 Ok(Zeroizing::new(plaintext))
180 }
181
182 #[must_use]
186 pub fn to_json(&self) -> String {
187 format!(
188 r#"{{"version":3,"id":"{}","address":"{}","crypto":{{"cipher":"aes-128-ctr","cipherparams":{{"iv":"{}"}},"ciphertext":"{}","kdf":"scrypt","kdfparams":{{"dklen":{},"n":{},"r":{},"p":{},"salt":"{}"}},"mac":"{}"}}}}"#,
189 self.id,
190 self.address.trim_start_matches("0x").to_lowercase(),
191 hex::encode(self.iv),
192 hex::encode(&self.ciphertext),
193 self.scrypt_params.dklen,
194 self.scrypt_params.n,
195 self.scrypt_params.r,
196 self.scrypt_params.p,
197 hex::encode(self.salt),
198 hex::encode(self.mac),
199 )
200 }
201}
202
203impl fmt::Debug for Keystore {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 f.debug_struct("Keystore")
206 .field("id", &self.id)
207 .field("address", &self.address)
208 .field("ciphertext", &"[REDACTED]")
209 .field("mac", &"[REDACTED]")
210 .finish()
211 }
212}
213
214fn derive_scrypt_key(
217 password: &[u8],
218 salt: &[u8],
219 params: &ScryptParams,
220) -> Result<Zeroizing<Vec<u8>>, SignerError> {
221 use scrypt::scrypt;
222 let log_n = (params.n as f64).log2() as u8;
223 let scrypt_params = scrypt::Params::new(log_n, params.r, params.p, params.dklen as usize)
224 .map_err(|e| SignerError::EncodingError(format!("scrypt params: {e}")))?;
225 let mut derived = Zeroizing::new(vec![0u8; params.dklen as usize]);
226 scrypt(password, salt, &scrypt_params, &mut derived)
227 .map_err(|e| SignerError::EncodingError(format!("scrypt: {e}")))?;
228 Ok(derived)
229}
230
231fn keccak256(data: &[u8]) -> [u8; 32] {
232 super::keccak256(data)
233}
234
235#[cfg(test)]
238#[allow(clippy::unwrap_used, clippy::expect_used)]
239mod tests {
240 use super::*;
241 use crate::traits::KeyPair;
242
243 fn light_params() -> ScryptParams {
244 ScryptParams::light()
245 }
246
247 #[test]
248 fn test_keystore_encrypt_decrypt_roundtrip() {
249 let signer = super::super::EthereumSigner::generate().unwrap();
250 let pk = signer.private_key_bytes();
251 let password = b"test-password-123";
252
253 let ks = Keystore::encrypt(&pk, password, &light_params()).unwrap();
254 let decrypted = ks.decrypt(password).unwrap();
255 assert_eq!(&*decrypted, &*pk);
256 }
257
258 #[test]
259 fn test_keystore_wrong_password_fails() {
260 let pk = [0x42u8; 32];
261 let ks = Keystore::encrypt(&pk, b"correct", &light_params()).unwrap();
262 let result = ks.decrypt(b"wrong");
263 assert!(result.is_err());
264 }
265
266 #[test]
267 fn test_keystore_address_matches() {
268 let signer = super::super::EthereumSigner::generate().unwrap();
269 let pk = signer.private_key_bytes();
270 let expected_addr = signer.address_checksum();
271
272 let ks = Keystore::encrypt(&pk, b"pw", &light_params()).unwrap();
273 assert_eq!(ks.address, expected_addr);
274 }
275
276 #[test]
277 fn test_keystore_to_json_format() {
278 let pk = [0x42u8; 32];
279 let ks = Keystore::encrypt(&pk, b"pw", &light_params()).unwrap();
280 let json = ks.to_json();
281 assert!(json.contains("\"version\":3"));
282 assert!(json.contains("\"cipher\":\"aes-128-ctr\""));
283 assert!(json.contains("\"kdf\":\"scrypt\""));
284 assert!(json.contains(&format!("\"n\":{}", light_params().n)));
285 }
286
287 #[test]
288 fn test_keystore_unique_salts() {
289 let pk = [0x42u8; 32];
290 let ks1 = Keystore::encrypt(&pk, b"pw", &light_params()).unwrap();
291 let ks2 = Keystore::encrypt(&pk, b"pw", &light_params()).unwrap();
292 assert_ne!(ks1.salt, ks2.salt, "salts should be unique");
293 assert_ne!(ks1.iv, ks2.iv, "IVs should be unique");
294 }
295
296 #[test]
297 fn test_keystore_invalid_key_length() {
298 assert!(Keystore::encrypt(&[0; 16], b"pw", &light_params()).is_err());
299 }
300}