diff --git a/Cargo.lock b/Cargo.lock index 30d9bb0e..5a2a4286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,12 +4,21 @@ version = 4 [[package]] name = "aead" -version = "0.5.2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "aead-stream" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "a901e5bcd15b4a9555a8f17a608d28ef92cf77bc4aa3b4a0c1a3fce1a9cc0bac" dependencies = [ - "crypto-common 0.1.7", - "generic-array", + "aead", ] [[package]] @@ -18,9 +27,9 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cipher 0.5.2", + "cipher", "cpubits", - "cpufeatures 0.3.0", + "cpufeatures", ] [[package]] @@ -194,12 +203,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - [[package]] name = "basic-toml" version = "0.1.10" @@ -224,6 +227,7 @@ dependencies = [ "arbitrary 0.4.7", "hashbrown 0.15.5", "rand 0.8.6", + "zeroize", ] [[package]] @@ -248,19 +252,10 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.3.0", + "cpufeatures", "zeroize", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.12.1" @@ -342,7 +337,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ - "cipher 0.5.2", + "cipher", ] [[package]] @@ -363,17 +358,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.1" @@ -381,43 +365,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cipher", + "cpufeatures", "rand_core 0.10.1", + "zeroize", ] [[package]] name = "chacha20poly1305" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" dependencies = [ "aead", - "chacha20 0.9.1", - "cipher 0.4.4", + "chacha20", + "cipher", "poly1305", "zeroize", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout 0.1.4", - "zeroize", -] - [[package]] name = "cipher" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer 0.12.1", - "crypto-common 0.2.2", - "inout 0.2.2", + "block-buffer", + "crypto-common", + "inout", ] [[package]] @@ -472,12 +447,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-oid" version = "0.10.2" @@ -496,15 +465,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -514,24 +474,15 @@ dependencies = [ "libc", ] -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.3", "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -545,15 +496,16 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.7", + "digest", "fiat-crypto", + "rand_core 0.10.1", "rustc_version", "subtle", "zeroize", @@ -570,16 +522,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "zeroize", -] - [[package]] name = "derive_arbitrary" version = "0.4.7" @@ -606,7 +548,7 @@ dependencies = [ name = "devolutions-crypto" version = "0.10.1" dependencies = [ - "aead", + "aead-stream", "aes", "arbitrary 1.4.2", "base64", @@ -630,7 +572,7 @@ dependencies = [ "rust-argon2", "scrypt", "serde-wasm-bindgen", - "sha2 0.11.0", + "sha2", "strum", "subtle", "thiserror", @@ -683,25 +625,15 @@ dependencies = [ "uniffi", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", - "const-oid 0.10.2", - "crypto-common 0.2.2", + "block-buffer", + "const-oid", + "crypto-common", "ctutils", ] @@ -713,25 +645,24 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ed25519" -version = "2.2.3" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ - "pkcs8", "signature", ] [[package]] name = "ed25519-dalek" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.6.4", - "serde", - "sha2 0.10.9", + "rand_core 0.10.1", + "sha2", + "signature", "subtle", "zeroize", ] @@ -760,9 +691,9 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fiat-crypto" -version = "0.2.9" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "find-msvc-tools" @@ -809,16 +740,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -904,7 +825,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.3", + "digest", ] [[package]] @@ -928,15 +849,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "inout" version = "0.2.2" @@ -1105,12 +1017,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "paste" version = "1.0.15" @@ -1123,7 +1029,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest 0.11.3", + "digest", "hmac", ] @@ -1139,16 +1045,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "plain" version = "0.2.3" @@ -1157,12 +1053,11 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "poly1305" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +checksum = "a00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2e" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", + "cpufeatures", "universal-hash", ] @@ -1231,7 +1126,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20 0.10.1", + "chacha20", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -1313,7 +1208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ "cfg-if", - "cipher 0.5.2", + "cipher", ] [[package]] @@ -1354,7 +1249,7 @@ dependencies = [ "cfg-if", "pbkdf2", "salsa20", - "sha2 0.11.0", + "sha2", ] [[package]] @@ -1430,17 +1325,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha2" version = "0.11.0" @@ -1448,8 +1332,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -1460,11 +1344,11 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signature" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.10.1", ] [[package]] @@ -1485,16 +1369,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -1820,12 +1694,12 @@ dependencies = [ [[package]] name = "universal-hash" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.1.7", - "subtle", + "crypto-common", + "ctutils", ] [[package]] @@ -1834,12 +1708,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -2015,13 +1883,12 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "x25519-dalek" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" dependencies = [ "curve25519-dalek", - "rand_core 0.6.4", - "serde", + "rand_core 0.10.1", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 15ed3a12..731c5ad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,30 +34,30 @@ name = "devolutions_crypto" crate-type = ["cdylib", "rlib"] [dependencies] -aead = { version = "0.5", features = ["stream"] } +aead-stream = { version = "0.6", features = ["alloc"] } aes = "0.9" base64 = "0.22" cbc = { version = "0.2", features = ["block-padding", "alloc"] } byteorder = "1" -chacha20poly1305 = "0.10" +chacha20poly1305 = { version = "0.11", features = ["zeroize"] } cfg-if = "1" hmac = "0.13" num_enum = "0.7" pbkdf2 = { version = "0.13", default-features = false } scrypt = { version = "0.12", default-features = false } -blahaj = { version = "0.6", default-features = false } +blahaj = { version = "0.6", default-features = false, features = ["zeroize_memory"] } sha2 = "0.11" strum = { version = "0.28", features = ["derive"] } subtle = "2" -zeroize = { version = "1.8" } +zeroize = { version = "1.8", features = ["derive"] } rand = "0.10" rand_08 = { package = "rand", version = "0.8" } thiserror = "2.0.12" typed-builder = "0.23.2" rust-argon2 = { workspace = true } -ed25519-dalek = { version = "2", features = [ "rand_core" ] } -x25519-dalek = { version = "2", features = [ "static_secrets" ] } +ed25519-dalek = { version = "3", features = [ "rand_core" ] } +x25519-dalek = { version = "3", features = [ "static_secrets" ] } # used for fuzzing arbitrary = { version = "1", features = ["derive"], optional = true } diff --git a/src/ciphertext/ciphertext_v2.rs b/src/ciphertext/ciphertext_v2.rs index 3f57d909..d86e600a 100644 --- a/src/ciphertext/ciphertext_v2.rs +++ b/src/ciphertext/ciphertext_v2.rs @@ -10,7 +10,7 @@ use super::Ciphertext; use std::convert::TryFrom; use chacha20poly1305::aead::{Aead, Payload}; -use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce}; +use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce}; use sha2::{Digest, Sha256}; use x25519_dalek::StaticSecret; @@ -97,7 +97,7 @@ impl CiphertextV2Symmetric { .try_fill_bytes(&mut nonce_bytes) .map_err(|_| Error::RandomError)?; - let nonce = XNonce::from_slice(&nonce_bytes); + let nonce = XNonce::from(nonce_bytes); // Authenticate the header let mut mac_data: Zeroizing> = Zeroizing::new(header.into()); @@ -110,9 +110,9 @@ impl CiphertextV2Symmetric { // Encrypt let ciphertext = { - let key = Key::from_slice(key.as_slice()); - let cipher = XChaCha20Poly1305::new(key); - cipher.encrypt(nonce, payload)? + let cipher = XChaCha20Poly1305::new_from_slice(key.as_slice()) + .expect("key length is hardcoded and correct"); + cipher.encrypt(&nonce, payload)? }; Ok(CiphertextV2Symmetric { @@ -136,11 +136,11 @@ impl CiphertextV2Symmetric { let result = { // Decrypt - let key = Key::from_slice(key.as_slice()); - let nonce = XNonce::from_slice(&self.nonce); + let nonce = XNonce::from(self.nonce); - let cipher = XChaCha20Poly1305::new(key); - cipher.decrypt(nonce, payload)? + let cipher = XChaCha20Poly1305::new_from_slice(key.as_slice()) + .expect("key length is hardcoded and correct"); + cipher.decrypt(&nonce, payload)? }; Ok(result) @@ -186,7 +186,7 @@ impl CiphertextV2Asymmetric { ) -> Result { let public_key = x25519_dalek::PublicKey::from(public_key); - let ephemeral_private_key = StaticSecret::random_from_rng(rand_08::rngs::OsRng); + let ephemeral_private_key = StaticSecret::from(*crate::utils::random_bytes::<32>()?); let ephemeral_public_key = x25519_dalek::PublicKey::from(&ephemeral_private_key); let key = ephemeral_private_key.diffie_hellman(&public_key); diff --git a/src/error.rs b/src/error.rs index 22c27e55..08b607f1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -125,8 +125,8 @@ impl From for Error { } } -impl From for Error { - fn from(_error: aead::Error) -> Error { +impl From for Error { + fn from(_error: chacha20poly1305::aead::Error) -> Error { Error::InvalidMac } } diff --git a/src/key/key_v1.rs b/src/key/key_v1.rs index 02a2469f..aa3526fa 100644 --- a/src/key/key_v1.rs +++ b/src/key/key_v1.rs @@ -3,6 +3,7 @@ use super::Error; use super::Result; use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroizing; use std::convert::TryFrom; @@ -53,7 +54,7 @@ impl<'a> Arbitrary<'a> for KeyV1Public { impl From for Vec { fn from(key: KeyV1Private) -> Self { - key.key.to_bytes().to_vec() + Zeroizing::new(key.key.to_bytes()).to_vec() } } @@ -71,10 +72,10 @@ impl TryFrom<&[u8]> for KeyV1Private { return Err(Error::InvalidLength); } - let mut key_bytes = [0u8; 32]; + let mut key_bytes = Zeroizing::new([0u8; 32]); key_bytes.copy_from_slice(&key[0..32]); Ok(Self { - key: StaticSecret::from(key_bytes), + key: StaticSecret::from(*key_bytes), }) } } @@ -96,7 +97,9 @@ impl TryFrom<&[u8]> for KeyV1Public { } pub fn generate_keypair() -> KeyV1Pair { - let private = StaticSecret::random_from_rng(rand_08::rngs::OsRng); + let bytes = + crate::utils::random_bytes::<32>().expect("the OS entropy source should be available"); + let private = StaticSecret::from(*bytes); let public = PublicKey::from(&private); KeyV1Pair { diff --git a/src/key/secret_key_v1.rs b/src/key/secret_key_v1.rs index 8ef022a4..cbe4424a 100644 --- a/src/key/secret_key_v1.rs +++ b/src/key/secret_key_v1.rs @@ -2,7 +2,6 @@ use super::Error; use super::Result; -use rand_08::RngCore; use zeroize::Zeroizing; use std::convert::TryFrom; @@ -33,8 +32,8 @@ impl<'a> Arbitrary<'a> for SecretKeyV1 { impl SecretKeyV1 { pub fn generate() -> Self { - let mut key = Zeroizing::new([0u8; 32]); - rand_08::rngs::OsRng.fill_bytes(key.as_mut()); + let key = + crate::utils::random_bytes::<32>().expect("the OS entropy source should be available"); Self { key } } @@ -57,10 +56,8 @@ impl TryFrom<&[u8]> for SecretKeyV1 { return Err(Error::InvalidLength); } - let mut key = [0u8; 32]; + let mut key = Zeroizing::new([0u8; 32]); key.copy_from_slice(data); - Ok(Self { - key: Zeroizing::new(key), - }) + Ok(Self { key }) } } diff --git a/src/online_ciphertext/mod.rs b/src/online_ciphertext/mod.rs index 1052b249..748174cc 100644 --- a/src/online_ciphertext/mod.rs +++ b/src/online_ciphertext/mod.rs @@ -1,43 +1,82 @@ -//! Module for symmetric/asymmetric encryption/decryption. +//! Module for chunked, streaming symmetric/asymmetric encryption/decryption. //! -//! This module contains everything related to encryption. You can use it to encrypt and decrypt data using either a shared key of a keypair. -//! Either way, the encryption will give you a `Ciphertext`, which has a method to decrypt it. +//! Unlike the [`ciphertext`](super::ciphertext) module, which encrypts a whole buffer at +//! once, this module processes the data one chunk at a time using the STREAM construction. +//! This lets you encrypt or decrypt data that does not fit in memory, or that arrives +//! progressively, without ever holding the full plaintext at once. +//! +//! You start by creating an `OnlineCiphertextEncryptor`, which produces an +//! `OnlineCiphertextHeader`. That header is not secret but is required to decrypt: keep it +//! alongside the ciphertext. You then feed each chunk to `encrypt_next_chunk`, and the final +//! (possibly smaller) chunk to `encrypt_last_chunk`. Every chunk fed to `encrypt_next_chunk` +//! must be exactly `chunk_size` bytes; the last chunk may be smaller. Each produced chunk is +//! `get_tag_size()` bytes larger than its input because of the authentication tag. //! //! ### Symmetric //! //! ```rust //! use devolutions_crypto::utils::generate_key; -//! use devolutions_crypto::ciphertext::{ encrypt, CiphertextVersion, Ciphertext }; +//! use devolutions_crypto::online_ciphertext::{ +//! OnlineCiphertextEncryptor, OnlineCiphertextHeader, OnlineCiphertextVersion, +//! }; +//! use std::convert::TryFrom; //! //! let key: Vec = generate_key(32).expect("generate key shouldn't fail"); +//! let chunk_size = 16u32; +//! +//! // Encrypt the data one chunk at a time. +//! let mut encryptor = +//! OnlineCiphertextEncryptor::new(&key, b"", chunk_size, OnlineCiphertextVersion::Latest) +//! .expect("creating the encryptor shouldn't fail"); +//! +//! // The header is required to decrypt; store or transmit it alongside the ciphertext. +//! let header: Vec = (&encryptor.get_header()).into(); //! -//! let data = b"somesecretdata"; +//! // Each `_next_chunk` input must be exactly `chunk_size` bytes; the last chunk may be smaller. +//! let chunk1 = encryptor.encrypt_next_chunk(b"0123456789abcdef", b"").expect("encrypting a chunk shouldn't fail"); +//! let chunk2 = encryptor.encrypt_last_chunk(b"the last chunk", b"").expect("encrypting the last chunk shouldn't fail"); //! -//! let encrypted_data: Ciphertext = encrypt(data, &key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); +//! // Decrypt using the header. +//! let header = OnlineCiphertextHeader::try_from(header.as_slice()).expect("the header should be valid"); +//! let mut decryptor = header.into_decryptor(&key, b"").expect("creating the decryptor shouldn't fail"); //! -//! let decrypted_data = encrypted_data.decrypt(&key).expect("The decryption shouldn't fail"); +//! let mut decrypted = decryptor.decrypt_next_chunk(&chunk1, b"").expect("decrypting a chunk shouldn't fail"); +//! decrypted.extend_from_slice(&decryptor.decrypt_last_chunk(&chunk2, b"").expect("decrypting the last chunk shouldn't fail")); //! -//! assert_eq!(decrypted_data, data); +//! assert_eq!(decrypted, b"0123456789abcdefthe last chunk"); //! ``` //! //! ### Asymmetric //! Here, you will need a `PublicKey` to encrypt data and the corresponding //! `PrivateKey` to decrypt it. You can generate them by using `generate_keypair` -//! in the [Key module](#key). +//! in the [Key module](super::key). //! //! ```rust //! use devolutions_crypto::key::{generate_keypair, KeyVersion, KeyPair}; -//! use devolutions_crypto::ciphertext::{ encrypt_asymmetric, CiphertextVersion, Ciphertext }; +//! use devolutions_crypto::online_ciphertext::{ +//! OnlineCiphertextEncryptor, OnlineCiphertextHeader, OnlineCiphertextVersion, +//! }; +//! use std::convert::TryFrom; //! //! let keypair: KeyPair = generate_keypair(KeyVersion::Latest); +//! let chunk_size = 16u32; +//! +//! let mut encryptor = OnlineCiphertextEncryptor::new_asymmetric( +//! &keypair.public_key, b"", chunk_size, OnlineCiphertextVersion::Latest, +//! ).expect("creating the encryptor shouldn't fail"); +//! +//! let header: Vec = (&encryptor.get_header()).into(); //! -//! let data = b"somesecretdata"; +//! let chunk1 = encryptor.encrypt_next_chunk(b"0123456789abcdef", b"").expect("encrypting a chunk shouldn't fail"); +//! let chunk2 = encryptor.encrypt_last_chunk(b"the last chunk", b"").expect("encrypting the last chunk shouldn't fail"); //! -//! let encrypted_data: Ciphertext = encrypt_asymmetric(data, &keypair.public_key, CiphertextVersion::Latest).expect("encryption shouldn't fail"); +//! let header = OnlineCiphertextHeader::try_from(header.as_slice()).expect("the header should be valid"); +//! let mut decryptor = header.into_decryptor_asymmetric(&keypair.private_key, b"").expect("creating the decryptor shouldn't fail"); //! -//! let decrypted_data = encrypted_data.decrypt_asymmetric(&keypair.private_key).expect("The decryption shouldn't fail"); +//! let mut decrypted = decryptor.decrypt_next_chunk(&chunk1, b"").expect("decrypting a chunk shouldn't fail"); +//! decrypted.extend_from_slice(&decryptor.decrypt_last_chunk(&chunk2, b"").expect("decrypting the last chunk shouldn't fail")); //! -//! assert_eq!(decrypted_data, data); +//! assert_eq!(decrypted, b"0123456789abcdefthe last chunk"); //! ``` mod online_ciphertext_v1; diff --git a/src/online_ciphertext/online_ciphertext_v1.rs b/src/online_ciphertext/online_ciphertext_v1.rs index a54f4d7e..65205d3c 100644 --- a/src/online_ciphertext/online_ciphertext_v1.rs +++ b/src/online_ciphertext/online_ciphertext_v1.rs @@ -7,10 +7,8 @@ use super::Result; use std::borrow::Borrow; -use chacha20poly1305::aead::{ - stream::{DecryptorLE31, EncryptorLE31}, - Payload, -}; +use aead_stream::{DecryptorLE31, EncryptorLE31}; +use chacha20poly1305::aead::Payload; use chacha20poly1305::{KeyInit, XChaCha20Poly1305}; use x25519_dalek::StaticSecret; @@ -304,7 +302,8 @@ impl OnlineCiphertextV1Encryptor { // Derive the key let key = Zeroizing::new(blake3::derive_key(CONTEXT, key)); - let cipher = XChaCha20Poly1305::new(key.as_ref().into()); + let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref()) + .expect("derived key length is hardcoded and correct"); // Create the STREAM encryptor let cipher = EncryptorLE31::from_aead(cipher, &nonce.into()); @@ -330,7 +329,7 @@ impl OnlineCiphertextV1Encryptor { // Perform a ECDH exchange as per ECIES let public_key = x25519_dalek::PublicKey::from(public_key); - let ephemeral_private_key = StaticSecret::random_from_rng(rand_08::rngs::OsRng); + let ephemeral_private_key = StaticSecret::from(*crate::utils::random_bytes::<32>()?); let ephemeral_public_key = x25519_dalek::PublicKey::from(&ephemeral_private_key); let key = ephemeral_private_key.diffie_hellman(&public_key); @@ -343,7 +342,8 @@ impl OnlineCiphertextV1Encryptor { // Derive the key let key = Zeroizing::new(blake3::derive_key(CONTEXT, key.as_bytes())); - let cipher = XChaCha20Poly1305::new(key.as_ref().into()); + let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref()) + .expect("derived key length is hardcoded and correct"); // Create the STREAM encryptor let cipher = EncryptorLE31::from_aead(cipher, &nonce.into()); @@ -369,7 +369,8 @@ impl OnlineCiphertextV1Decryptor { pub fn new(key: &[u8], mut aad: Vec, header: OnlineCiphertextV1HeaderSymmetric) -> Self { // Derive the key let key = Zeroizing::new(blake3::derive_key(CONTEXT, key)); - let cipher = XChaCha20Poly1305::new(key.as_ref().into()); + let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref()) + .expect("derived key length is hardcoded and correct"); // Create the STREAM decryptor let cipher = DecryptorLE31::from_aead(cipher, &header.nonce.into()); @@ -396,7 +397,8 @@ impl OnlineCiphertextV1Decryptor { // Derive the key let key = Zeroizing::new(blake3::derive_key(CONTEXT, key.as_bytes())); - let cipher = XChaCha20Poly1305::new(key.as_ref().into()); + let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref()) + .expect("derived key length is hardcoded and correct"); // Create the STREAM decryptor let cipher = DecryptorLE31::from_aead(cipher, &header.nonce.into()); diff --git a/src/secret_sharing/secret_sharing_v1.rs b/src/secret_sharing/secret_sharing_v1.rs index 389a04f1..ace56377 100644 --- a/src/secret_sharing/secret_sharing_v1.rs +++ b/src/secret_sharing/secret_sharing_v1.rs @@ -10,9 +10,7 @@ use zeroize::Zeroizing; #[cfg(feature = "fuzz")] use arbitrary::Arbitrary; -// This will need some work in the Sharks crate to get the zeroize working. -//#[derive(Zeroize)] -//#[zeroize(drop)] +// `blahaj::Share` zeroizes on drop (`zeroize_memory` feature); `threshold` is not secret. #[derive(Clone)] pub struct ShareV1 { threshold: u8, @@ -42,8 +40,11 @@ impl core::fmt::Debug for ShareV1 { impl From for Vec { fn from(share: ShareV1) -> Vec { - let mut data: Vec = vec![share.threshold]; - data.append(&mut (&share.share).into()); + let share_bytes: Zeroizing> = Zeroizing::new((&share.share).into()); + + let mut data: Vec = Vec::with_capacity(1 + share_bytes.len()); + data.push(share.threshold); + data.extend_from_slice(&share_bytes); data } diff --git a/src/signing_key/signing_key_v1.rs b/src/signing_key/signing_key_v1.rs index 7e85b12a..5f08a070 100644 --- a/src/signing_key/signing_key_v1.rs +++ b/src/signing_key/signing_key_v1.rs @@ -3,6 +3,7 @@ use super::Error; use super::Result; use ed25519_dalek::{SigningKey, VerifyingKey}; +use zeroize::Zeroizing; use std::convert::TryFrom; @@ -58,7 +59,7 @@ impl<'a> Arbitrary<'a> for SigningKeyV1Public { impl From for Vec { fn from(key: SigningKeyV1Pair) -> Self { - key.keypair.to_keypair_bytes().to_vec() + Zeroizing::new(key.keypair.to_keypair_bytes()).to_vec() } } @@ -99,9 +100,10 @@ impl TryFrom<&[u8]> for SigningKeyV1Public { } pub fn generate_signing_keypair() -> SigningKeyV1Pair { - let mut csprng = rand_08::rngs::OsRng; + let secret = + crate::utils::random_bytes::<32>().expect("the OS entropy source should be available"); - let keypair = SigningKey::generate(&mut csprng); + let keypair = SigningKey::from_bytes(&secret); SigningKeyV1Pair { keypair } } diff --git a/src/utils.rs b/src/utils.rs index 15e3ac9f..df294086 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,6 +10,7 @@ use pbkdf2::pbkdf2; use rand::TryRng; use sha2::Sha256; use subtle::ConstantTimeEq as _; +use zeroize::Zeroizing; use crate::online_ciphertext::OnlineCiphertextHeader; @@ -38,6 +39,19 @@ pub fn generate_key(length: usize) -> Result> { Ok(key) } +/// Fills a fixed-size buffer directly from the operating system's CSPRNG. +/// +/// Used for private key generation: drawing the bytes straight from the OS +/// avoids any intermediate userspace CSPRNG state that could be used to +/// reconstruct the key. The buffer is zeroized on drop. +pub(crate) fn random_bytes() -> Result> { + let mut bytes = Zeroizing::new([0u8; N]); + rand::rngs::SysRng + .try_fill_bytes(bytes.as_mut_slice()) + .map_err(|_| Error::RandomError)?; + Ok(bytes) +} + /// Derives a password or key into a new one using PBKDF2. /// # Arguments /// * `key` - The key or password to derive. diff --git a/tests/conformity.rs b/tests/conformity.rs index e189b9b2..1109cd50 100644 --- a/tests/conformity.rs +++ b/tests/conformity.rs @@ -347,3 +347,51 @@ fn test_symmetric_decrypt_with_secret_key_v2() { assert_eq!(result, b"test Ciph3rtext~2"); } + +#[test] +fn test_online_ciphertext_decrypt_symmetric_v1() { + // Cross-version STREAM known-answer vector (key "Key123", chunk size 1000): locks the + // STREAM-LE31 wire format so a crate bump that silently changes it is caught here. + use devolutions_crypto::online_ciphertext::OnlineCiphertextHeader; + + let key = b"Key123"; + let tag_size = 16usize; + + let header_bytes = general_purpose::STANDARD + .decode("DQwHAAEAAADoAwAAOU63lZRBVkAapzo92zfg/KZy6/o=") + .unwrap(); + + let ciphertext = general_purpose::STANDARD + .decode("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") + .unwrap(); + + let expected_plaintext = general_purpose::STANDARD + .decode("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gTnVsbGFtIHF1YW0gbnVuYywgcGxhY2VyYXQgbWF4aW11cyBmYWNpbGlzaXMgZGlnbmlzc2ltLCB0aW5jaWR1bnQgaW4gdHVycGlzLiBEdWlzIHRlbXBvciBtYWduYSBldSBjb252YWxsaXMgbWF0dGlzLiBOdWxsYW0gcmhvbmN1cyBsYWN1cyBub24gbmliaCBtb2xlc3RpZSBzb2RhbGVzLiBJbiBub24gdHJpc3RpcXVlIG51bmMuIE51bGxhIGlkIGdyYXZpZGEgbnVuYy4gRG9uZWMgZWdlc3RhcyBtaSBlZ2V0IHRlbGx1cyBlZmZpY2l0dXIsIG1vbGxpcyBsb2JvcnRpcyB1cm5hIHRyaXN0aXF1ZS4gVml2YW11cyBzZWQgdGVsbHVzIHZpdGFlIG1ldHVzIG1hdHRpcyBsYWNpbmlhLiBWZXN0aWJ1bHVtIGRhcGlidXMgZXN0IGV1IHB1bHZpbmFyIGxhb3JlZXQuIEV0aWFtIGNvbW1vZG8gZXJvcyBhdCBmYWNpbGlzaXMgbW9sbGlzLiBDdXJhYml0dXIgcnV0cnVtIHRpbmNpZHVudCBzZW0sIHZpdGFlIHBsYWNlcmF0IG51bmMgbGFjaW5pYSBuZWMuIE9yY2kgdmFyaXVzIG5hdG9xdWUgcGVuYXRpYnVzIGV0IG1hZ25pcyBkaXMgcGFydHVyaWVudCBtb250ZXMsIG5hc2NldHVyIHJpZGljdWx1cyBtdXMuIFBoYXNlbGx1cyBjdXJzdXMgYXVndWUgdXQgbmlzbCB0ZW1wdXMgbGFjaW5pYS4gCk51bGxhIHZpdGFlIGZlcm1lbnR1bSBlc3QsIGV0IHZlaGljdWxhIG1hdXJpcy4gSW50ZXJkdW0gZXQgbWFsZXN1YWRhIGZhbWVzIGFjIGFudGUgaXBzdW0gcHJpbWlzIGluIGZhdWNpYnVzLiBQaGFzZWxsdXMgc2l0IGFtZXQgbWFnbmEgcGVsbGVudGVzcXVlLCBtYXhpbXVzIGp1c3RvIHZpdGFlLCBtYWxlc3VhZGEgYW50ZS4gVmVzdGlidWx1bSBhbnRlIGlwc3VtIHByaW1pcyBpbiBmYXVjaWJ1cyBvcmNpIGx1Y3R1cyBldCB1bHRyaWNlcyBwb3N1ZXJlIGN1YmlsaWEgY3VyYWU7IFN1c3BlbmRpc3NlIHRlbXB1cyBkb2xvciBldSBhdWd1ZSByaG9uY3VzIHBoYXJldHJhLiBEb25lYyB2aXRhZSBuaXNpIHBlbGxlbnRlc3F1ZSwgY29udmFsbGlzIG9kaW8gYXQsIGludGVyZHVtIGF1Z3VlLiBQZWxsZW50ZXNxdWUgYXQgcHVydXMgYSBuaWJoIGxhY2luaWEgdWxsYW1jb3JwZXIgYWMgYSBudWxsYS4gQWVuZWFuIG5pYmggbGlndWxhLCBoZW5kcmVyaXQgaW4gbHVjdHVzIGV0LCBzb2RhbGVzIG5vbiB0dXJwaXMuIE51bGxhIGZhY2lsaXNpLiBOdWxsYSBsYW9yZWV0IG1hc3NhIGZlbGlzLCBhIGRpY3R1bSBsaWd1bGEgYmxhbmRpdCB1dC4gClNlZCBzb2RhbGVzIHJpc3VzIGp1c3RvLCB1dCBmZXJtZW50dW0gc2FwaWVuIGRpY3R1bSBzZWQuIFNlZCB1bHRyaWNlcyB2ZWxpdCBldCBmZWxpcyBmZXJtZW50dW0gaW50ZXJkdW0uIFZlc3RpYnVsdW0gY29uc2VjdGV0dXIgbGFvcmVldCBpcHN1bS4gUGVsbGVudGVzcXVlIGhhYml0YW50IG1vcmJpIHRyaXN0aXF1ZSBzZW5lY3R1cyBldCBuZXR1cyBldCBtYWxlc3VhZGEgZmFtZXMgYWMgdHVycGlzIGVnZXN0YXMuIENyYXMgaW4gZXggc3VzY2lwaXQsIGFjY3Vtc2FuIHB1cnVzIHZpdGFlLCByaG9uY3VzIGVsaXQuIE1hZWNlbmFzIHF1aXMgc2FwaWVuIG5vbiBxdWFtIGFjY3Vtc2FuIHZlaGljdWxhIHNlZCB2aXRhZSBwdXJ1cy4gUHJvaW4gY29uc2VxdWF0IGlwc3VtIGluIGxhY3VzIGxvYm9ydGlzIHRlbXB1cy4gRG9uZWMgYWMgYWxpcXVldCBtYXNzYS4g") + .unwrap(); + + let header = OnlineCiphertextHeader::try_from(header_bytes.as_slice()).unwrap(); + let chunk_with_tag = header.get_chunk_size() as usize + tag_size; + + let mut decryptor = header.into_decryptor(key, b"").unwrap(); + + let mut result = Vec::new(); + let mut offset = 0; + + // Every chunk but the last is a full `chunk_size + tag` block. + while ciphertext.len() - offset > chunk_with_tag { + result.extend_from_slice( + &decryptor + .decrypt_next_chunk(&ciphertext[offset..offset + chunk_with_tag], b"") + .unwrap(), + ); + offset += chunk_with_tag; + } + + result.extend_from_slice( + &decryptor + .decrypt_last_chunk(&ciphertext[offset..], b"") + .unwrap(), + ); + + assert_eq!(result, expected_plaintext); +}