Skip to main content

max / balanced_breakfast

bb-core: bump aes-gcm to 0.11 Closes the crypto pair; sha2 landed in 48c21e3. Workspace tests green, clippy clean. Three API breaks: aead no longer re-exports OsRng, AeadCore::generate_nonce is gone, and Nonce::from_slice is deprecated in favour of TryFrom. Nonce generation now draws from the crate's own rand, matching how synckit-client seals, and the decrypt path surfaces a malformed nonce as an error rather than through a panicking conversion. This envelope protects config secrets already on user disks, so the bump is gated on a known-answer test rather than roundtrips, which re-encrypt with the same code and would pass even if the envelope changed shape. The vector comes from python cryptography's AES-GCM, pinned in field_matches_independent_known_answer with a wrong-key check so it also shows the field stays bound to its key. Verified both directions: the pinned 0.10-era ciphertext decrypts under 0.11, and a field sealed by the rewritten nonce path decrypts under the independent implementation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 19:06 UTC
Commit: ead7a7587455cdd859eddefc6fa0d3ba03180bfc
Parent: 6a6212f
3 files changed, +112 insertions, -63 deletions
M Cargo.lock +73 -56
@@ -10,12 +10,12 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10 10
11 11 [[package]]
12 12 name = "aead"
13 - version = "0.5.2"
13 + version = "0.6.1"
14 14 source = "registry+https://github.com/rust-lang/crates.io-index"
15 - checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
15 + checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99"
16 16 dependencies = [
17 - "crypto-common 0.1.7",
18 - "generic-array",
17 + "crypto-common 0.2.2",
18 + "inout 0.2.2",
19 19 ]
20 20
21 21 [[package]]
@@ -25,19 +25,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
25 25 checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
26 26 dependencies = [
27 27 "cfg-if",
28 - "cipher",
28 + "cipher 0.4.4",
29 29 "cpufeatures 0.2.17",
30 30 ]
31 31
32 32 [[package]]
33 + name = "aes"
34 + version = "0.9.1"
35 + source = "registry+https://github.com/rust-lang/crates.io-index"
36 + checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138"
37 + dependencies = [
38 + "cipher 0.5.2",
39 + "cpubits",
40 + "cpufeatures 0.3.0",
41 + ]
42 +
43 + [[package]]
33 44 name = "aes-gcm"
34 - version = "0.10.3"
45 + version = "0.11.0"
35 46 source = "registry+https://github.com/rust-lang/crates.io-index"
36 - checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
47 + checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028"
37 48 dependencies = [
38 49 "aead",
39 - "aes",
40 - "cipher",
50 + "aes 0.9.1",
51 + "cipher 0.5.2",
41 52 "ctr",
42 53 "ghash",
43 54 "subtle",
@@ -675,7 +686,7 @@ version = "0.1.2"
675 686 source = "registry+https://github.com/rust-lang/crates.io-index"
676 687 checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
677 688 dependencies = [
678 - "cipher",
689 + "cipher 0.4.4",
679 690 ]
680 691
681 692 [[package]]
@@ -723,37 +734,26 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
723 734
724 735 [[package]]
725 736 name = "chacha20"
726 - version = "0.9.1"
727 - source = "registry+https://github.com/rust-lang/crates.io-index"
728 - checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
729 - dependencies = [
730 - "cfg-if",
731 - "cipher",
732 - "cpufeatures 0.2.17",
733 - ]
734 -
735 - [[package]]
736 - name = "chacha20"
737 737 version = "0.10.1"
738 738 source = "registry+https://github.com/rust-lang/crates.io-index"
739 739 checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
740 740 dependencies = [
741 741 "cfg-if",
742 + "cipher 0.5.2",
742 743 "cpufeatures 0.3.0",
743 744 "rand_core 0.10.1",
744 745 ]
745 746
746 747 [[package]]
747 748 name = "chacha20poly1305"
748 - version = "0.10.1"
749 + version = "0.11.0"
749 750 source = "registry+https://github.com/rust-lang/crates.io-index"
750 - checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
751 + checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb"
751 752 dependencies = [
752 753 "aead",
753 - "chacha20 0.9.1",
754 - "cipher",
754 + "chacha20",
755 + "cipher 0.5.2",
755 756 "poly1305",
756 - "zeroize",
757 757 ]
758 758
759 759 [[package]]
@@ -777,8 +777,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
777 777 checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
778 778 dependencies = [
779 779 "crypto-common 0.1.7",
780 - "inout",
781 - "zeroize",
780 + "inout 0.1.4",
781 + ]
782 +
783 + [[package]]
784 + name = "cipher"
785 + version = "0.5.2"
786 + source = "registry+https://github.com/rust-lang/crates.io-index"
787 + checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
788 + dependencies = [
789 + "block-buffer 0.12.1",
790 + "crypto-common 0.2.2",
791 + "inout 0.2.2",
782 792 ]
783 793
784 794 [[package]]
@@ -899,6 +909,12 @@ dependencies = [
899 909 ]
900 910
901 911 [[package]]
912 + name = "cpubits"
913 + version = "0.1.1"
914 + source = "registry+https://github.com/rust-lang/crates.io-index"
915 + checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
916 +
917 + [[package]]
902 918 name = "cpufeatures"
903 919 version = "0.2.17"
904 920 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -977,7 +993,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
977 993 checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
978 994 dependencies = [
979 995 "generic-array",
980 - "rand_core 0.6.4",
981 996 "typenum",
982 997 ]
983 998
@@ -987,7 +1002,9 @@ version = "0.2.2"
987 1002 source = "registry+https://github.com/rust-lang/crates.io-index"
988 1003 checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
989 1004 dependencies = [
1005 + "getrandom 0.4.3",
990 1006 "hybrid-array",
1007 + "rand_core 0.10.1",
991 1008 ]
992 1009
993 1010 [[package]]
@@ -1095,11 +1112,11 @@ dependencies = [
1095 1112
1096 1113 [[package]]
1097 1114 name = "ctr"
1098 - version = "0.9.2"
1115 + version = "0.10.1"
1099 1116 source = "registry+https://github.com/rust-lang/crates.io-index"
1100 - checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
1117 + checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21"
1101 1118 dependencies = [
1102 - "cipher",
1119 + "cipher 0.5.2",
1103 1120 ]
1104 1121
1105 1122 [[package]]
@@ -1941,11 +1958,10 @@ dependencies = [
1941 1958
1942 1959 [[package]]
1943 1960 name = "ghash"
1944 - version = "0.5.1"
1961 + version = "0.6.0"
1945 1962 source = "registry+https://github.com/rust-lang/crates.io-index"
1946 - checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
1963 + checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5"
1947 1964 dependencies = [
1948 - "opaque-debug",
1949 1965 "polyval",
1950 1966 ]
1951 1967
@@ -2597,6 +2613,15 @@ dependencies = [
2597 2613 ]
2598 2614
2599 2615 [[package]]
2616 + name = "inout"
2617 + version = "0.2.2"
2618 + source = "registry+https://github.com/rust-lang/crates.io-index"
2619 + checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
2620 + dependencies = [
2621 + "hybrid-array",
2622 + ]
2623 +
2624 + [[package]]
2600 2625 name = "ipnet"
2601 2626 version = "2.11.0"
2602 2627 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3524,12 +3549,6 @@ dependencies = [
3524 3549 ]
3525 3550
3526 3551 [[package]]
3527 - name = "opaque-debug"
3528 - version = "0.3.1"
3529 - source = "registry+https://github.com/rust-lang/crates.io-index"
3530 - checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
3531 -
3532 - [[package]]
3533 3552 name = "open"
3534 3553 version = "5.3.3"
3535 3554 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4001,24 +4020,22 @@ dependencies = [
4001 4020
4002 4021 [[package]]
4003 4022 name = "poly1305"
4004 - version = "0.8.0"
4023 + version = "0.9.1"
4005 4024 source = "registry+https://github.com/rust-lang/crates.io-index"
4006 - checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
4025 + checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c"
4007 4026 dependencies = [
4008 - "cpufeatures 0.2.17",
4009 - "opaque-debug",
4027 + "cpufeatures 0.3.0",
4010 4028 "universal-hash",
4011 4029 ]
4012 4030
4013 4031 [[package]]
4014 4032 name = "polyval"
4015 - version = "0.6.2"
4033 + version = "0.7.3"
4016 4034 source = "registry+https://github.com/rust-lang/crates.io-index"
4017 - checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
4035 + checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd"
4018 4036 dependencies = [
4019 - "cfg-if",
4020 - "cpufeatures 0.2.17",
4021 - "opaque-debug",
4037 + "cpubits",
4038 + "cpufeatures 0.3.0",
4022 4039 "universal-hash",
4023 4040 ]
4024 4041
@@ -4243,7 +4260,7 @@ version = "0.10.2"
4243 4260 source = "registry+https://github.com/rust-lang/crates.io-index"
4244 4261 checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
4245 4262 dependencies = [
4246 - "chacha20 0.10.1",
4263 + "chacha20",
4247 4264 "getrandom 0.4.3",
4248 4265 "rand_core 0.10.1",
4249 4266 ]
@@ -4781,7 +4798,7 @@ version = "5.1.0"
4781 4798 source = "registry+https://github.com/rust-lang/crates.io-index"
4782 4799 checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14"
4783 4800 dependencies = [
4784 - "aes",
4801 + "aes 0.8.4",
4785 4802 "cbc",
4786 4803 "futures-util",
4787 4804 "generic-array",
@@ -6741,12 +6758,12 @@ checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
6741 6758
6742 6759 [[package]]
6743 6760 name = "universal-hash"
6744 - version = "0.5.1"
6761 + version = "0.6.1"
6745 6762 source = "registry+https://github.com/rust-lang/crates.io-index"
6746 - checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
6763 + checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96"
6747 6764 dependencies = [
6748 - "crypto-common 0.1.7",
6749 - "subtle",
6765 + "crypto-common 0.2.2",
6766 + "ctutils",
6750 6767 ]
6751 6768
6752 6769 [[package]]
M Cargo.toml +1 -1
@@ -32,7 +32,7 @@ sqlx = { version = "0.9", features = ["runtime-tokio", "sqlite", "chrono", "uuid
32 32 roxmltree = "0.21"
33 33
34 34 # Encryption
35 - aes-gcm = "0.10"
35 + aes-gcm = "0.11"
36 36 base64 = "0.22"
37 37 rand = "0.10"
38 38
@@ -7,8 +7,8 @@
7 7 //! If absent, returns the value as-is (plaintext passthrough). This allows existing
8 8 //! configs to work without migration.
9 9
10 - use aes_gcm::aead::{Aead, OsRng};
11 - use aes_gcm::{AeadCore, Aes256Gcm, KeyInit};
10 + use aes_gcm::aead::Aead;
11 + use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
12 12 use base64::Engine;
13 13 use base64::engine::general_purpose::STANDARD as BASE64;
14 14 use bb_interface::{ConfigFieldType, ConfigSchema};
@@ -160,14 +160,18 @@ pub fn load_or_create_key_from_keychain(file_path: &Path) -> Result<EncryptionKe
160 160 /// Encrypt a plaintext string field using AES-256-GCM
161 161 pub fn encrypt_field(plaintext: &str, key: &EncryptionKey) -> Result<String, String> {
162 162 let cipher = Aes256Gcm::new((&**key).into());
163 - let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
163 + // aes-gcm 0.11 dropped AeadCore::generate_nonce and aead's OsRng re-export,
164 + // so the nonce is drawn from the crate's own CSPRNG.
165 + let mut nonce_bytes = [0u8; 12];
166 + rand::rng().fill_bytes(&mut nonce_bytes);
167 + let nonce = Nonce::from(nonce_bytes);
164 168 let ciphertext = cipher
165 169 .encrypt(&nonce, plaintext.as_bytes())
166 170 .map_err(|e| format!("Encryption failed: {e}"))?;
167 171
168 172 // nonce (12 bytes) || ciphertext+tag
169 173 let mut payload = Vec::with_capacity(12 + ciphertext.len());
170 - payload.extend_from_slice(&nonce);
174 + payload.extend_from_slice(&nonce_bytes);
171 175 payload.extend_from_slice(&ciphertext);
172 176
173 177 Ok(format!("{}{}", PREFIX, BASE64.encode(&payload)))
@@ -189,11 +193,12 @@ pub fn decrypt_field(value: &str, key: &EncryptionKey) -> Result<String, String>
189 193 }
190 194
191 195 let (nonce_bytes, ciphertext) = payload.split_at(12);
192 - let nonce = aes_gcm::Nonce::from_slice(nonce_bytes);
196 + let nonce = Nonce::try_from(nonce_bytes)
197 + .map_err(|e| format!("Encrypted payload has a malformed nonce: {e}"))?;
193 198 let cipher = Aes256Gcm::new((&**key).into());
194 199
195 200 let plaintext = cipher
196 - .decrypt(nonce, ciphertext)
201 + .decrypt(&nonce, ciphertext)
197 202 .map_err(|e| format!("Decryption failed: {e}"))?;
198 203
199 204 String::from_utf8(plaintext).map_err(|e| format!("Decrypted data is not valid UTF-8: {e}"))
@@ -273,6 +278,33 @@ mod tests {
273 278 use super::*;
274 279 use bb_interface::ConfigField;
275 280
281 + /// Known-answer vector for the at-rest field envelope, computed outside
282 + /// RustCrypto (python `cryptography`'s AES-GCM). Every encrypted config
283 + /// secret already on a user's disk is sealed this way, so the envelope must
284 + /// keep decrypting byte-for-byte across cipher upgrades. The roundtrip test
285 + /// below re-encrypts with the same code and would pass even if the envelope
286 + /// changed shape.
287 + #[test]
288 + fn field_matches_independent_known_answer() {
289 + const STORED: &str =
290 + "bb_enc:v1:ICEiIyQlJicoKSorsFiGEwP2fGd9XDGromqRjfA/jfDy5VrIuJCkEeUh+ps/4a3i44U=";
291 + let mut raw = [0u8; 32];
292 + for (i, b) in raw.iter_mut().enumerate() {
293 + *b = u8::try_from(i).unwrap();
294 + }
295 + let key: EncryptionKey = Zeroizing::new(raw);
296 +
297 + assert_eq!(
298 + decrypt_field(STORED, &key).unwrap(),
299 + "bb config secret value"
300 + );
301 +
302 + // A different key must fail, so the vector shows the field is actually
303 + // bound to the stored key rather than merely decodable.
304 + let other: EncryptionKey = Zeroizing::new([9u8; 32]);
305 + assert!(decrypt_field(STORED, &other).is_err());
306 + }
307 +
276 308 #[test]
277 309 fn roundtrip_encrypt_decrypt() {
278 310 let key = generate_key();