Skip to content

Commit fbb3f71

Browse files
chore(deps): Update crypto dependencies to chacha20poly1305 0.11 and dalek v3 (#408)
- chacha20poly1305 0.10 -> 0.11, ed25519-dalek 2 -> 3, x25519-dalek 2 -> 3, STREAM now from the aead-stream crate (removed from aead 0.6) - private keys are now generated directly from the OS CSPRNG - zeroize hardening: cipher key schedule (chacha20poly1305/zeroize), secret shares (blahaj/zeroize_memory), serialization intermediates - online_ciphertext doctests now use the STREAM API - added a cross-version STREAM conformity vector Wire formats are unchanged and covered by conformity vectors. Note: dalek v3 types appear in public From impls and MSRV rises to 1.85, so the Rust API is semver-breaking.
1 parent 05399a9 commit fbb3f71

12 files changed

Lines changed: 235 additions & 262 deletions

File tree

‎Cargo.lock‎

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

‎Cargo.toml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,30 +34,30 @@ name = "devolutions_crypto"
3434
crate-type = ["cdylib", "rlib"]
3535

3636
[dependencies]
37-
aead = { version = "0.5", features = ["stream"] }
37+
aead-stream = { version = "0.6", features = ["alloc"] }
3838
aes = "0.9"
3939
base64 = "0.22"
4040
cbc = { version = "0.2", features = ["block-padding", "alloc"] }
4141
byteorder = "1"
42-
chacha20poly1305 = "0.10"
42+
chacha20poly1305 = { version = "0.11", features = ["zeroize"] }
4343
cfg-if = "1"
4444
hmac = "0.13"
4545
num_enum = "0.7"
4646
pbkdf2 = { version = "0.13", default-features = false }
4747
scrypt = { version = "0.12", default-features = false }
48-
blahaj = { version = "0.6", default-features = false }
48+
blahaj = { version = "0.6", default-features = false, features = ["zeroize_memory"] }
4949
sha2 = "0.11"
5050
strum = { version = "0.28", features = ["derive"] }
5151
subtle = "2"
52-
zeroize = { version = "1.8" }
52+
zeroize = { version = "1.8", features = ["derive"] }
5353
rand = "0.10"
5454
rand_08 = { package = "rand", version = "0.8" }
5555
thiserror = "2.0.12"
5656
typed-builder = "0.23.2"
5757
rust-argon2 = { workspace = true }
5858

59-
ed25519-dalek = { version = "2", features = [ "rand_core" ] }
60-
x25519-dalek = { version = "2", features = [ "static_secrets" ] }
59+
ed25519-dalek = { version = "3", features = [ "rand_core" ] }
60+
x25519-dalek = { version = "3", features = [ "static_secrets" ] }
6161

6262
# used for fuzzing
6363
arbitrary = { version = "1", features = ["derive"], optional = true }

‎src/ciphertext/ciphertext_v2.rs‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use super::Ciphertext;
1010
use std::convert::TryFrom;
1111

1212
use chacha20poly1305::aead::{Aead, Payload};
13-
use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce};
13+
use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce};
1414

1515
use sha2::{Digest, Sha256};
1616
use x25519_dalek::StaticSecret;
@@ -97,7 +97,7 @@ impl CiphertextV2Symmetric {
9797
.try_fill_bytes(&mut nonce_bytes)
9898
.map_err(|_| Error::RandomError)?;
9999

100-
let nonce = XNonce::from_slice(&nonce_bytes);
100+
let nonce = XNonce::from(nonce_bytes);
101101

102102
// Authenticate the header
103103
let mut mac_data: Zeroizing<Vec<u8>> = Zeroizing::new(header.into());
@@ -110,9 +110,9 @@ impl CiphertextV2Symmetric {
110110

111111
// Encrypt
112112
let ciphertext = {
113-
let key = Key::from_slice(key.as_slice());
114-
let cipher = XChaCha20Poly1305::new(key);
115-
cipher.encrypt(nonce, payload)?
113+
let cipher = XChaCha20Poly1305::new_from_slice(key.as_slice())
114+
.expect("key length is hardcoded and correct");
115+
cipher.encrypt(&nonce, payload)?
116116
};
117117

118118
Ok(CiphertextV2Symmetric {
@@ -136,11 +136,11 @@ impl CiphertextV2Symmetric {
136136

137137
let result = {
138138
// Decrypt
139-
let key = Key::from_slice(key.as_slice());
140-
let nonce = XNonce::from_slice(&self.nonce);
139+
let nonce = XNonce::from(self.nonce);
141140

142-
let cipher = XChaCha20Poly1305::new(key);
143-
cipher.decrypt(nonce, payload)?
141+
let cipher = XChaCha20Poly1305::new_from_slice(key.as_slice())
142+
.expect("key length is hardcoded and correct");
143+
cipher.decrypt(&nonce, payload)?
144144
};
145145

146146
Ok(result)
@@ -186,7 +186,7 @@ impl CiphertextV2Asymmetric {
186186
) -> Result<Self> {
187187
let public_key = x25519_dalek::PublicKey::from(public_key);
188188

189-
let ephemeral_private_key = StaticSecret::random_from_rng(rand_08::rngs::OsRng);
189+
let ephemeral_private_key = StaticSecret::from(*crate::utils::random_bytes::<32>()?);
190190
let ephemeral_public_key = x25519_dalek::PublicKey::from(&ephemeral_private_key);
191191

192192
let key = ephemeral_private_key.diffie_hellman(&public_key);

‎src/error.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ impl From<UnpadError> for Error {
125125
}
126126
}
127127

128-
impl From<aead::Error> for Error {
129-
fn from(_error: aead::Error) -> Error {
128+
impl From<chacha20poly1305::aead::Error> for Error {
129+
fn from(_error: chacha20poly1305::aead::Error) -> Error {
130130
Error::InvalidMac
131131
}
132132
}

‎src/key/key_v1.rs‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use super::Error;
33
use super::Result;
44

55
use x25519_dalek::{PublicKey, StaticSecret};
6+
use zeroize::Zeroizing;
67

78
use std::convert::TryFrom;
89

@@ -53,7 +54,7 @@ impl<'a> Arbitrary<'a> for KeyV1Public {
5354

5455
impl From<KeyV1Private> for Vec<u8> {
5556
fn from(key: KeyV1Private) -> Self {
56-
key.key.to_bytes().to_vec()
57+
Zeroizing::new(key.key.to_bytes()).to_vec()
5758
}
5859
}
5960

@@ -71,10 +72,10 @@ impl TryFrom<&[u8]> for KeyV1Private {
7172
return Err(Error::InvalidLength);
7273
}
7374

74-
let mut key_bytes = [0u8; 32];
75+
let mut key_bytes = Zeroizing::new([0u8; 32]);
7576
key_bytes.copy_from_slice(&key[0..32]);
7677
Ok(Self {
77-
key: StaticSecret::from(key_bytes),
78+
key: StaticSecret::from(*key_bytes),
7879
})
7980
}
8081
}
@@ -96,7 +97,9 @@ impl TryFrom<&[u8]> for KeyV1Public {
9697
}
9798

9899
pub fn generate_keypair() -> KeyV1Pair {
99-
let private = StaticSecret::random_from_rng(rand_08::rngs::OsRng);
100+
let bytes =
101+
crate::utils::random_bytes::<32>().expect("the OS entropy source should be available");
102+
let private = StaticSecret::from(*bytes);
100103
let public = PublicKey::from(&private);
101104

102105
KeyV1Pair {

‎src/key/secret_key_v1.rs‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
use super::Error;
33
use super::Result;
44

5-
use rand_08::RngCore;
65
use zeroize::Zeroizing;
76

87
use std::convert::TryFrom;
@@ -33,8 +32,8 @@ impl<'a> Arbitrary<'a> for SecretKeyV1 {
3332

3433
impl SecretKeyV1 {
3534
pub fn generate() -> Self {
36-
let mut key = Zeroizing::new([0u8; 32]);
37-
rand_08::rngs::OsRng.fill_bytes(key.as_mut());
35+
let key =
36+
crate::utils::random_bytes::<32>().expect("the OS entropy source should be available");
3837
Self { key }
3938
}
4039

@@ -57,10 +56,8 @@ impl TryFrom<&[u8]> for SecretKeyV1 {
5756
return Err(Error::InvalidLength);
5857
}
5958

60-
let mut key = [0u8; 32];
59+
let mut key = Zeroizing::new([0u8; 32]);
6160
key.copy_from_slice(data);
62-
Ok(Self {
63-
key: Zeroizing::new(key),
64-
})
61+
Ok(Self { key })
6562
}
6663
}

‎src/online_ciphertext/mod.rs‎

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,82 @@
1-
//! Module for symmetric/asymmetric encryption/decryption.
1+
//! Module for chunked, streaming symmetric/asymmetric encryption/decryption.
22
//!
3-
//! This module contains everything related to encryption. You can use it to encrypt and decrypt data using either a shared key of a keypair.
4-
//! Either way, the encryption will give you a `Ciphertext`, which has a method to decrypt it.
3+
//! Unlike the [`ciphertext`](super::ciphertext) module, which encrypts a whole buffer at
4+
//! once, this module processes the data one chunk at a time using the STREAM construction.
5+
//! This lets you encrypt or decrypt data that does not fit in memory, or that arrives
6+
//! progressively, without ever holding the full plaintext at once.
7+
//!
8+
//! You start by creating an `OnlineCiphertextEncryptor`, which produces an
9+
//! `OnlineCiphertextHeader`. That header is not secret but is required to decrypt: keep it
10+
//! alongside the ciphertext. You then feed each chunk to `encrypt_next_chunk`, and the final
11+
//! (possibly smaller) chunk to `encrypt_last_chunk`. Every chunk fed to `encrypt_next_chunk`
12+
//! must be exactly `chunk_size` bytes; the last chunk may be smaller. Each produced chunk is
13+
//! `get_tag_size()` bytes larger than its input because of the authentication tag.
514
//!
615
//! ### Symmetric
716
//!
817
//! ```rust
918
//! use devolutions_crypto::utils::generate_key;
10-
//! use devolutions_crypto::ciphertext::{ encrypt, CiphertextVersion, Ciphertext };
19+
//! use devolutions_crypto::online_ciphertext::{
20+
//! OnlineCiphertextEncryptor, OnlineCiphertextHeader, OnlineCiphertextVersion,
21+
//! };
22+
//! use std::convert::TryFrom;
1123
//!
1224
//! let key: Vec<u8> = generate_key(32).expect("generate key shouldn't fail");
25+
//! let chunk_size = 16u32;
26+
//!
27+
//! // Encrypt the data one chunk at a time.
28+
//! let mut encryptor =
29+
//! OnlineCiphertextEncryptor::new(&key, b"", chunk_size, OnlineCiphertextVersion::Latest)
30+
//! .expect("creating the encryptor shouldn't fail");
31+
//!
32+
//! // The header is required to decrypt; store or transmit it alongside the ciphertext.
33+
//! let header: Vec<u8> = (&encryptor.get_header()).into();
1334
//!
14-
//! let data = b"somesecretdata";
35+
//! // Each `_next_chunk` input must be exactly `chunk_size` bytes; the last chunk may be smaller.
36+
//! let chunk1 = encryptor.encrypt_next_chunk(b"0123456789abcdef", b"").expect("encrypting a chunk shouldn't fail");
37+
//! let chunk2 = encryptor.encrypt_last_chunk(b"the last chunk", b"").expect("encrypting the last chunk shouldn't fail");
1538
//!
16-
//! let encrypted_data: Ciphertext = encrypt(data, &key, CiphertextVersion::Latest).expect("encryption shouldn't fail");
39+
//! // Decrypt using the header.
40+
//! let header = OnlineCiphertextHeader::try_from(header.as_slice()).expect("the header should be valid");
41+
//! let mut decryptor = header.into_decryptor(&key, b"").expect("creating the decryptor shouldn't fail");
1742
//!
18-
//! let decrypted_data = encrypted_data.decrypt(&key).expect("The decryption shouldn't fail");
43+
//! let mut decrypted = decryptor.decrypt_next_chunk(&chunk1, b"").expect("decrypting a chunk shouldn't fail");
44+
//! decrypted.extend_from_slice(&decryptor.decrypt_last_chunk(&chunk2, b"").expect("decrypting the last chunk shouldn't fail"));
1945
//!
20-
//! assert_eq!(decrypted_data, data);
46+
//! assert_eq!(decrypted, b"0123456789abcdefthe last chunk");
2147
//! ```
2248
//!
2349
//! ### Asymmetric
2450
//! Here, you will need a `PublicKey` to encrypt data and the corresponding
2551
//! `PrivateKey` to decrypt it. You can generate them by using `generate_keypair`
26-
//! in the [Key module](#key).
52+
//! in the [Key module](super::key).
2753
//!
2854
//! ```rust
2955
//! use devolutions_crypto::key::{generate_keypair, KeyVersion, KeyPair};
30-
//! use devolutions_crypto::ciphertext::{ encrypt_asymmetric, CiphertextVersion, Ciphertext };
56+
//! use devolutions_crypto::online_ciphertext::{
57+
//! OnlineCiphertextEncryptor, OnlineCiphertextHeader, OnlineCiphertextVersion,
58+
//! };
59+
//! use std::convert::TryFrom;
3160
//!
3261
//! let keypair: KeyPair = generate_keypair(KeyVersion::Latest);
62+
//! let chunk_size = 16u32;
63+
//!
64+
//! let mut encryptor = OnlineCiphertextEncryptor::new_asymmetric(
65+
//! &keypair.public_key, b"", chunk_size, OnlineCiphertextVersion::Latest,
66+
//! ).expect("creating the encryptor shouldn't fail");
67+
//!
68+
//! let header: Vec<u8> = (&encryptor.get_header()).into();
3369
//!
34-
//! let data = b"somesecretdata";
70+
//! let chunk1 = encryptor.encrypt_next_chunk(b"0123456789abcdef", b"").expect("encrypting a chunk shouldn't fail");
71+
//! let chunk2 = encryptor.encrypt_last_chunk(b"the last chunk", b"").expect("encrypting the last chunk shouldn't fail");
3572
//!
36-
//! let encrypted_data: Ciphertext = encrypt_asymmetric(data, &keypair.public_key, CiphertextVersion::Latest).expect("encryption shouldn't fail");
73+
//! let header = OnlineCiphertextHeader::try_from(header.as_slice()).expect("the header should be valid");
74+
//! let mut decryptor = header.into_decryptor_asymmetric(&keypair.private_key, b"").expect("creating the decryptor shouldn't fail");
3775
//!
38-
//! let decrypted_data = encrypted_data.decrypt_asymmetric(&keypair.private_key).expect("The decryption shouldn't fail");
76+
//! let mut decrypted = decryptor.decrypt_next_chunk(&chunk1, b"").expect("decrypting a chunk shouldn't fail");
77+
//! decrypted.extend_from_slice(&decryptor.decrypt_last_chunk(&chunk2, b"").expect("decrypting the last chunk shouldn't fail"));
3978
//!
40-
//! assert_eq!(decrypted_data, data);
79+
//! assert_eq!(decrypted, b"0123456789abcdefthe last chunk");
4180
//! ```
4281
4382
mod online_ciphertext_v1;

‎src/online_ciphertext/online_ciphertext_v1.rs‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ use super::Result;
77

88
use std::borrow::Borrow;
99

10-
use chacha20poly1305::aead::{
11-
stream::{DecryptorLE31, EncryptorLE31},
12-
Payload,
13-
};
10+
use aead_stream::{DecryptorLE31, EncryptorLE31};
11+
use chacha20poly1305::aead::Payload;
1412
use chacha20poly1305::{KeyInit, XChaCha20Poly1305};
1513

1614
use x25519_dalek::StaticSecret;
@@ -304,7 +302,8 @@ impl OnlineCiphertextV1Encryptor {
304302

305303
// Derive the key
306304
let key = Zeroizing::new(blake3::derive_key(CONTEXT, key));
307-
let cipher = XChaCha20Poly1305::new(key.as_ref().into());
305+
let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref())
306+
.expect("derived key length is hardcoded and correct");
308307

309308
// Create the STREAM encryptor
310309
let cipher = EncryptorLE31::from_aead(cipher, &nonce.into());
@@ -330,7 +329,7 @@ impl OnlineCiphertextV1Encryptor {
330329
// Perform a ECDH exchange as per ECIES
331330
let public_key = x25519_dalek::PublicKey::from(public_key);
332331

333-
let ephemeral_private_key = StaticSecret::random_from_rng(rand_08::rngs::OsRng);
332+
let ephemeral_private_key = StaticSecret::from(*crate::utils::random_bytes::<32>()?);
334333
let ephemeral_public_key = x25519_dalek::PublicKey::from(&ephemeral_private_key);
335334

336335
let key = ephemeral_private_key.diffie_hellman(&public_key);
@@ -343,7 +342,8 @@ impl OnlineCiphertextV1Encryptor {
343342

344343
// Derive the key
345344
let key = Zeroizing::new(blake3::derive_key(CONTEXT, key.as_bytes()));
346-
let cipher = XChaCha20Poly1305::new(key.as_ref().into());
345+
let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref())
346+
.expect("derived key length is hardcoded and correct");
347347

348348
// Create the STREAM encryptor
349349
let cipher = EncryptorLE31::from_aead(cipher, &nonce.into());
@@ -369,7 +369,8 @@ impl OnlineCiphertextV1Decryptor {
369369
pub fn new(key: &[u8], mut aad: Vec<u8>, header: OnlineCiphertextV1HeaderSymmetric) -> Self {
370370
// Derive the key
371371
let key = Zeroizing::new(blake3::derive_key(CONTEXT, key));
372-
let cipher = XChaCha20Poly1305::new(key.as_ref().into());
372+
let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref())
373+
.expect("derived key length is hardcoded and correct");
373374

374375
// Create the STREAM decryptor
375376
let cipher = DecryptorLE31::from_aead(cipher, &header.nonce.into());
@@ -396,7 +397,8 @@ impl OnlineCiphertextV1Decryptor {
396397

397398
// Derive the key
398399
let key = Zeroizing::new(blake3::derive_key(CONTEXT, key.as_bytes()));
399-
let cipher = XChaCha20Poly1305::new(key.as_ref().into());
400+
let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref())
401+
.expect("derived key length is hardcoded and correct");
400402

401403
// Create the STREAM decryptor
402404
let cipher = DecryptorLE31::from_aead(cipher, &header.nonce.into());

‎src/secret_sharing/secret_sharing_v1.rs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ use zeroize::Zeroizing;
1010
#[cfg(feature = "fuzz")]
1111
use arbitrary::Arbitrary;
1212

13-
// This will need some work in the Sharks crate to get the zeroize working.
14-
//#[derive(Zeroize)]
15-
//#[zeroize(drop)]
13+
// `blahaj::Share` zeroizes on drop (`zeroize_memory` feature); `threshold` is not secret.
1614
#[derive(Clone)]
1715
pub struct ShareV1 {
1816
threshold: u8,
@@ -42,8 +40,11 @@ impl core::fmt::Debug for ShareV1 {
4240

4341
impl From<ShareV1> for Vec<u8> {
4442
fn from(share: ShareV1) -> Vec<u8> {
45-
let mut data: Vec<u8> = vec![share.threshold];
46-
data.append(&mut (&share.share).into());
43+
let share_bytes: Zeroizing<Vec<u8>> = Zeroizing::new((&share.share).into());
44+
45+
let mut data: Vec<u8> = Vec::with_capacity(1 + share_bytes.len());
46+
data.push(share.threshold);
47+
data.extend_from_slice(&share_bytes);
4748

4849
data
4950
}

‎src/signing_key/signing_key_v1.rs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use super::Error;
33
use super::Result;
44

55
use ed25519_dalek::{SigningKey, VerifyingKey};
6+
use zeroize::Zeroizing;
67

78
use std::convert::TryFrom;
89

@@ -58,7 +59,7 @@ impl<'a> Arbitrary<'a> for SigningKeyV1Public {
5859

5960
impl From<SigningKeyV1Pair> for Vec<u8> {
6061
fn from(key: SigningKeyV1Pair) -> Self {
61-
key.keypair.to_keypair_bytes().to_vec()
62+
Zeroizing::new(key.keypair.to_keypair_bytes()).to_vec()
6263
}
6364
}
6465

@@ -99,9 +100,10 @@ impl TryFrom<&[u8]> for SigningKeyV1Public {
99100
}
100101

101102
pub fn generate_signing_keypair() -> SigningKeyV1Pair {
102-
let mut csprng = rand_08::rngs::OsRng;
103+
let secret =
104+
crate::utils::random_bytes::<32>().expect("the OS entropy source should be available");
103105

104-
let keypair = SigningKey::generate(&mut csprng);
106+
let keypair = SigningKey::from_bytes(&secret);
105107

106108
SigningKeyV1Pair { keypair }
107109
}

0 commit comments

Comments
 (0)