Skip to content

Commit 18f1bea

Browse files
authored
feat(rust): Add struct for XChaCha20Poly1305 key (#5365)
1 parent a2740dc commit 18f1bea

File tree

5 files changed

+168
-24
lines changed

5 files changed

+168
-24
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ironfish-rust/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ rand = "0.8.5"
5353
tiny-bip39 = "1.0"
5454
xxhash-rust = { version = "0.8.5", features = ["xxh3"] }
5555
argon2 = { version = "0.5.3", features = ["password-hash"] }
56+
hkdf = "0.12.4"
57+
sha2 = "0.10"
5658

5759
[dev-dependencies]
5860
hex-literal = "0.4"

ironfish-rust/src/errors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub enum IronfishErrorKind {
3232
FailedSignatureVerification,
3333
FailedXChaCha20Poly1305Decryption,
3434
FailedXChaCha20Poly1305Encryption,
35+
FailedHkdfExpansion,
3536
IllegalValue,
3637
InconsistentWitness,
3738
InvalidAssetIdentifier,

ironfish-rust/src/xchacha20poly1305.rs

Lines changed: 151 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,140 @@
44

55
use std::io;
66

7+
use argon2::RECOMMENDED_SALT_LEN;
78
use argon2::{password_hash::SaltString, Argon2};
89
use chacha20poly1305::aead::Aead;
910
use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce};
11+
use hkdf::Hkdf;
1012
use rand::{thread_rng, RngCore};
13+
use sha2::Sha256;
1114

1215
use crate::errors::{IronfishError, IronfishErrorKind};
1316

14-
const KEY_LENGTH: usize = 32;
15-
const NONCE_LENGTH: usize = 24;
17+
pub const KEY_LENGTH: usize = 32;
18+
pub const SALT_LENGTH: usize = RECOMMENDED_SALT_LEN;
19+
pub const XNONCE_LENGTH: usize = 24;
20+
21+
#[derive(Debug)]
22+
pub struct XChaCha20Poly1305Key {
23+
pub key: [u8; KEY_LENGTH],
24+
25+
pub nonce: [u8; XNONCE_LENGTH],
26+
27+
pub salt: [u8; SALT_LENGTH],
28+
}
29+
30+
impl XChaCha20Poly1305Key {
31+
pub fn generate(passphrase: &[u8]) -> Result<XChaCha20Poly1305Key, IronfishError> {
32+
let mut nonce = [0u8; XNONCE_LENGTH];
33+
thread_rng().fill_bytes(&mut nonce);
34+
35+
let mut salt = [0u8; SALT_LENGTH];
36+
thread_rng().fill_bytes(&mut salt);
37+
38+
XChaCha20Poly1305Key::from_parts(passphrase, salt, nonce)
39+
}
40+
41+
pub fn from_parts(
42+
passphrase: &[u8],
43+
salt: [u8; SALT_LENGTH],
44+
nonce: [u8; XNONCE_LENGTH],
45+
) -> Result<XChaCha20Poly1305Key, IronfishError> {
46+
let mut key = [0u8; KEY_LENGTH];
47+
let argon2 = Argon2::default();
48+
49+
argon2
50+
.hash_password_into(passphrase, &salt, &mut key)
51+
.map_err(|_| IronfishError::new(IronfishErrorKind::FailedArgon2Hash))?;
52+
53+
Ok(XChaCha20Poly1305Key { key, salt, nonce })
54+
}
55+
56+
pub fn derive_key(&self, salt: [u8; SALT_LENGTH]) -> Result<[u8; KEY_LENGTH], IronfishError> {
57+
let hkdf = Hkdf::<Sha256>::new(None, &self.key);
58+
59+
let mut okm = [0u8; KEY_LENGTH];
60+
hkdf.expand(&salt, &mut okm)
61+
.map_err(|_| IronfishError::new(IronfishErrorKind::FailedHkdfExpansion))?;
62+
63+
Ok(okm)
64+
}
65+
66+
pub fn derive_new_key(&self) -> Result<XChaCha20Poly1305Key, IronfishError> {
67+
let mut nonce = [0u8; XNONCE_LENGTH];
68+
thread_rng().fill_bytes(&mut nonce);
69+
70+
let mut salt = [0u8; SALT_LENGTH];
71+
thread_rng().fill_bytes(&mut salt);
72+
73+
let hkdf = Hkdf::<Sha256>::new(None, &self.key);
74+
75+
let mut okm = [0u8; KEY_LENGTH];
76+
hkdf.expand(&salt, &mut okm)
77+
.map_err(|_| IronfishError::new(IronfishErrorKind::FailedHkdfExpansion))?;
78+
79+
Ok(XChaCha20Poly1305Key {
80+
key: okm,
81+
salt,
82+
nonce,
83+
})
84+
}
85+
86+
pub fn read<R: io::Read>(mut reader: R) -> Result<Self, IronfishError> {
87+
let mut salt = [0u8; SALT_LENGTH];
88+
reader.read_exact(&mut salt)?;
89+
90+
let mut nonce = [0u8; XNONCE_LENGTH];
91+
reader.read_exact(&mut nonce)?;
92+
93+
let mut key = [0u8; KEY_LENGTH];
94+
reader.read_exact(&mut key)?;
95+
96+
Ok(XChaCha20Poly1305Key { salt, nonce, key })
97+
}
98+
99+
pub fn write<W: io::Write>(&self, mut writer: W) -> Result<(), IronfishError> {
100+
writer.write_all(&self.salt)?;
101+
writer.write_all(&self.nonce)?;
102+
writer.write_all(&self.key)?;
103+
104+
Ok(())
105+
}
106+
107+
pub fn destroy(&mut self) {
108+
self.key.fill(0);
109+
self.nonce.fill(0);
110+
self.salt.fill(0);
111+
}
112+
113+
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, IronfishError> {
114+
let nonce = XNonce::from_slice(&self.nonce);
115+
let key = Key::from(self.key);
116+
let cipher = XChaCha20Poly1305::new(&key);
117+
118+
let ciphertext = cipher.encrypt(nonce, plaintext).map_err(|_| {
119+
IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Encryption)
120+
})?;
121+
122+
Ok(ciphertext)
123+
}
124+
125+
pub fn decrypt(&self, ciphertext: Vec<u8>) -> Result<Vec<u8>, IronfishError> {
126+
let nonce = XNonce::from_slice(&self.nonce);
127+
let key = Key::from(self.key);
128+
let cipher = XChaCha20Poly1305::new(&key);
129+
130+
cipher
131+
.decrypt(nonce, ciphertext.as_ref())
132+
.map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Decryption))
133+
}
134+
}
16135

17136
#[derive(Debug)]
18137
pub struct EncryptOutput {
19138
pub salt: Vec<u8>,
20139

21-
pub nonce: [u8; NONCE_LENGTH],
140+
pub nonce: [u8; XNONCE_LENGTH],
22141

23142
pub ciphertext: Vec<u8>,
24143
}
@@ -46,7 +165,7 @@ impl EncryptOutput {
46165
let mut salt = vec![0u8; salt_len];
47166
reader.read_exact(&mut salt)?;
48167

49-
let mut nonce = [0u8; NONCE_LENGTH];
168+
let mut nonce = [0u8; XNONCE_LENGTH];
50169
reader.read_exact(&mut nonce)?;
51170

52171
let mut ciphertext_len = [0u8; 4];
@@ -88,7 +207,7 @@ pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result<EncryptOutput, Iro
88207
let key = derive_key(passphrase, salt_bytes)?;
89208

90209
let cipher = XChaCha20Poly1305::new(&key);
91-
let mut nonce_bytes = [0u8; NONCE_LENGTH];
210+
let mut nonce_bytes = [0u8; XNONCE_LENGTH];
92211
thread_rng().fill_bytes(&mut nonce_bytes);
93212
let nonce = XNonce::from_slice(&nonce_bytes);
94213

@@ -119,19 +238,22 @@ pub fn decrypt(
119238

120239
#[cfg(test)]
121240
mod test {
122-
use crate::xchacha20poly1305::{decrypt, encrypt};
123-
124-
use super::EncryptOutput;
241+
use crate::xchacha20poly1305::XChaCha20Poly1305Key;
125242

126243
#[test]
127244
fn test_valid_passphrase() {
128245
let plaintext = "thisissensitivedata";
129246
let passphrase = "supersecretpassword";
130247

131-
let encrypted_output = encrypt(plaintext.as_bytes(), passphrase.as_bytes())
248+
let key =
249+
XChaCha20Poly1305Key::generate(passphrase.as_bytes()).expect("should generate key");
250+
251+
let encrypted_output = key
252+
.encrypt(plaintext.as_bytes())
132253
.expect("should successfully encrypt");
133-
let decrypted =
134-
decrypt(encrypted_output, passphrase.as_bytes()).expect("should decrypt successfully");
254+
let decrypted = key
255+
.decrypt(encrypted_output)
256+
.expect("should decrypt successfully");
135257

136258
assert_eq!(decrypted, plaintext.as_bytes());
137259
}
@@ -142,28 +264,33 @@ mod test {
142264
let passphrase = "supersecretpassword";
143265
let incorrect_passphrase = "foobar";
144266

145-
let encrypted_output = encrypt(plaintext.as_bytes(), passphrase.as_bytes())
267+
let key =
268+
XChaCha20Poly1305Key::generate(passphrase.as_bytes()).expect("should generate key");
269+
270+
let encrypted_output = key
271+
.encrypt(plaintext.as_bytes())
146272
.expect("should successfully encrypt");
147273

148-
decrypt(encrypted_output, incorrect_passphrase.as_bytes())
274+
let incorrect_key = XChaCha20Poly1305Key::generate(incorrect_passphrase.as_bytes())
275+
.expect("should generate key");
276+
277+
incorrect_key
278+
.decrypt(encrypted_output)
149279
.expect_err("should fail decryption");
150280
}
151281

152282
#[test]
153-
fn test_encrypt_output_serialization() {
154-
let plaintext = "thisissensitivedata";
283+
fn test_derive_key() {
155284
let passphrase = "supersecretpassword";
156285

157-
let encrypted_output = encrypt(plaintext.as_bytes(), passphrase.as_bytes())
158-
.expect("should successfully encrypt");
159-
160-
let mut vec: Vec<u8> = vec![];
161-
encrypted_output
162-
.write(&mut vec)
163-
.expect("should serialize successfully");
286+
let encryption_key = XChaCha20Poly1305Key::generate(passphrase.as_bytes())
287+
.expect("should successfully generate key");
164288

165-
let deserialized = EncryptOutput::read(&vec[..]).expect("should deserialize successfully");
289+
let key = encryption_key.derive_new_key().expect("should derive key");
290+
let derived_key = encryption_key
291+
.derive_key(key.salt)
292+
.expect("should derive key");
166293

167-
assert_eq!(encrypted_output, deserialized);
294+
assert_eq!(key.key, derived_key);
168295
}
169296
}

supply-chain/config.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@ criteria = "safe-to-deploy"
390390
version = "0.1.19"
391391
criteria = "safe-to-deploy"
392392

393+
[[exemptions.hkdf]]
394+
version = "0.12.4"
395+
criteria = "safe-to-deploy"
396+
393397
[[exemptions.hmac]]
394398
version = "0.11.0"
395399
criteria = "safe-to-deploy"

0 commit comments

Comments
 (0)