Skip to main content

encryption/
factory.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3//! 主密钥工厂:从 32 字节主密钥派生各算法独立子密钥,避免同一密钥跨算法复用
4//! (对应 PHP 版 `EncryptionManagerFactory`)。
5//!
6//! # 两套派生方案
7//!
8//! 共同的不变量:**每个算法一把独立子密钥** —— 同主密钥 + 同标签恒得同一子密钥,
9//! 不同算法(标签与构造都不同)的子密钥互不相通,因此密钥永不跨算法复用。
10//! 两套方案只在构造与冻结状态上不同:
11//!
12//! 1. **旧标签(五个默认算法,冻结)**:
13//!    `子密钥 = HMAC-SHA256(key = 主密钥, msg = 用途标签)`,16 字节密钥取派生
14//!    结果前 16 字节。这五个标签自 1.x 起被存量密文绑定 —— 改标签或改参数顺序
15//!    都会让存量数据再也解不开,故永不改动。也不复刻原版历史遗留的 v1 反序方案
16//!    (标签作 key)—— 本 crate 采用 Rust 独立格式。
17//! 2. **扩展标签(九个扩展算法)**:HKDF-Expand(RFC 5869),域与旧标签隔离:
18//!    `PRK = HMAC-SHA256(key = 主密钥, msg = "dgn:derive-ext")`(扩展域专用
19//!    PRK,标签同样不承载秘密),`子密钥 = HKDF-Expand(PRK, info = 标签,
20//!    L = 密钥长度)`。
21//!
22//! 扩展为何用 HKDF 而非再截一段 HMAC:Threefish-512 需要 64 字节密钥,单次
23//! HMAC-SHA256 只给 32 字节;HKDF-Expand 正是「一次提取、按需多块展开」的标准
24//! 构造(上限 255 × 32 字节),不必为超出块长的情形自造串联规则。
25//!
26//! 扩展标签统一命名 `dgn:derive-ext:<注册标识>`,与旧标签(`dgn:derive:` 前缀、
27//! 无 `-ext`)以及彼此都不重名。
28
29use hkdf::Hkdf;
30use hmac::{Hmac, Mac};
31use sha2::Sha256;
32use zeroize::Zeroizing;
33
34use crate::contract::SymmetricCipher;
35use crate::encryptor::{
36    Aes256CbcEncryptor, Aes256GcmEncryptor, Aes256GcmSivEncryptor, Aria256CbcEncryptor,
37    Camellia256CbcEncryptor, ChaCha20Poly1305IetfEncryptor, Kuznyechik256CbcEncryptor,
38    Salsa20Encryptor, SodiumXChaCha20Encryptor, Threefish512CbcEncryptor,
39};
40use crate::error::{Error, Result};
41use crate::guomi::{Sm4CbcEncryptor, Sm4GcmEncryptor, Zuc128Encryptor, Zuc256Encryptor};
42use crate::manager::EncryptionManager;
43use crate::registry::Registry;
44
45// 旧用途标签(与原版一致,**冻结不可改**;标签不承载秘密,秘密始终是主密钥)。
46const LABEL_AES_GCM: &[u8] = b"dgn:derive:aes-gcm";
47const LABEL_AES_CBC: &[u8] = b"dgn:derive:aes-cbc";
48const LABEL_SM4: &[u8] = b"dgn:derive:sm4";
49const LABEL_ZUC: &[u8] = b"dgn:derive:zuc";
50const LABEL_SODIUM: &[u8] = b"dgn:derive:sodium";
51
52/// 扩展域的 PRK 标签:`PRK = HMAC-SHA256(key = 主密钥, msg = 本常量)`。
53const LABEL_PRK_EXT: &[u8] = b"dgn:derive-ext";
54
55// 扩展标签:HKDF-Expand 的 info,命名统一为 `dgn:derive-ext:<注册标识>`。
56const LABEL_CHACHA20_POLY1305_IETF: &[u8] = b"dgn:derive-ext:chacha20-poly1305-ietf";
57const LABEL_AES_GCM_SIV: &[u8] = b"dgn:derive-ext:aes-256-gcm-siv";
58const LABEL_CAMELLIA: &[u8] = b"dgn:derive-ext:camellia-256-cbc-hmac";
59const LABEL_ARIA: &[u8] = b"dgn:derive-ext:aria-256-cbc-hmac";
60const LABEL_THREEFISH: &[u8] = b"dgn:derive-ext:threefish-512-cbc-hmac";
61const LABEL_KUZNYECHIK: &[u8] = b"dgn:derive-ext:kuznyechik-256-cbc-hmac";
62const LABEL_SALSA20: &[u8] = b"dgn:derive-ext:salsa20";
63const LABEL_SM4_GCM: &[u8] = b"dgn:derive-ext:sm4-gcm";
64const LABEL_ZUC256: &[u8] = b"dgn:derive-ext:zuc-256";
65
66/// 从一把主密钥构建 [`EncryptionManager`]。
67///
68/// [`EncryptionManagerFactory::from_master_key`] 注册五个默认算法
69/// (`aes-256-gcm`、`aes-256-cbc-hmac`、`sodium-xchacha20` 与国密的
70/// `sm4-cbc`、`zuc-128`),行为与 1.x 完全一致;
71/// [`EncryptionManagerFactory::from_master_key_all`] 再补上九个扩展算法(共
72/// 十四个);[`EncryptionManagerFactory::register_extensions`] 只把九个扩展
73/// 注册进已有注册表,便于自行组合。各方法的 `default_identifier` 必须已注册。
74pub struct EncryptionManagerFactory;
75
76impl EncryptionManagerFactory {
77    /// 五个默认算法(与 1.x 行为一致,存量密文照旧解开)。
78    pub fn from_master_key(
79        master_key: &[u8],
80        default_identifier: &str,
81    ) -> Result<EncryptionManager> {
82        let master_key = master_key_32(master_key)?;
83
84        let mut registry: Registry<Box<dyn SymmetricCipher>> = Registry::new("symmetric cipher");
85        register_default_ciphers(&mut registry, &master_key);
86
87        EncryptionManager::new(registry, default_identifier)
88    }
89
90    /// 全部十四个算法:五个默认算法 + 九个扩展算法。
91    ///
92    /// 与 [`Self::from_master_key`] 相比,五个默认算法的子密钥逐字节相同
93    /// (同一套冻结标签派生),扩展算法各用 HKDF 派生的独立子密钥。
94    ///
95    /// ```
96    /// use encryption::factory::EncryptionManagerFactory;
97    ///
98    /// let manager =
99    ///     EncryptionManagerFactory::from_master_key_all(&[7u8; 32], "aes-256-gcm")?;
100    /// assert_eq!(manager.registry().identifiers().len(), 14);
101    ///
102    /// let blob = manager.encrypt_with("threefish-512-cbc-hmac", b"data")?;
103    /// assert_eq!(
104    ///     manager.decrypt_with("threefish-512-cbc-hmac", &blob)?,
105    ///     b"data"
106    /// );
107    /// # Ok::<(), encryption::Error>(())
108    /// ```
109    pub fn from_master_key_all(
110        master_key: &[u8],
111        default_identifier: &str,
112    ) -> Result<EncryptionManager> {
113        let master_key = master_key_32(master_key)?;
114
115        let mut registry: Registry<Box<dyn SymmetricCipher>> = Registry::new("symmetric cipher");
116        register_default_ciphers(&mut registry, &master_key);
117        register_extension_ciphers(&mut registry, &master_key);
118
119        EncryptionManager::new(registry, default_identifier)
120    }
121
122    /// 把九个扩展算法注册进已有注册表,供自行组合(例如在
123    /// [`Self::from_master_key`] 的五个默认算法之上按需扩展,或与自研算法共存)。
124    /// 主密钥必须为 32 字节,否则 [`Error::InvalidMasterKey`]。
125    ///
126    /// ```
127    /// use encryption::factory::EncryptionManagerFactory;
128    ///
129    /// let mut manager = EncryptionManagerFactory::from_master_key(&[7u8; 32], "aes-256-gcm")?;
130    /// EncryptionManagerFactory::register_extensions(manager.registry_mut(), &[7u8; 32])?;
131    /// assert_eq!(manager.registry().identifiers().len(), 14);
132    /// # Ok::<(), encryption::Error>(())
133    /// ```
134    pub fn register_extensions(
135        registry: &mut Registry<Box<dyn SymmetricCipher>>,
136        master_key: &[u8],
137    ) -> Result<()> {
138        let master_key = master_key_32(master_key)?;
139        register_extension_ciphers(registry, &master_key);
140        Ok(())
141    }
142}
143
144/// 校验主密钥长度并转成定长数组;拷贝包在 [`Zeroizing`] 里,作用域结束
145/// (含提前返回)即清零,堆/栈残留里不留主密钥。
146fn master_key_32(master_key: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
147    let key: [u8; 32] = master_key.try_into().map_err(|_| Error::InvalidMasterKey {
148        got: master_key.len(),
149    })?;
150    Ok(Zeroizing::new(key))
151}
152
153/// 五个默认算法(旧标签派生)。
154fn register_default_ciphers(
155    registry: &mut Registry<Box<dyn SymmetricCipher>>,
156    master_key: &[u8; 32],
157) {
158    registry.register(Box::new(Aes256GcmEncryptor::new(derive_subkey(
159        master_key,
160        LABEL_AES_GCM,
161    ))));
162    registry.register(Box::new(Aes256CbcEncryptor::new(derive_subkey(
163        master_key,
164        LABEL_AES_CBC,
165    ))));
166    registry.register(Box::new(SodiumXChaCha20Encryptor::new(derive_subkey(
167        master_key,
168        LABEL_SODIUM,
169    ))));
170    registry.register(Box::new(Sm4CbcEncryptor::new(derive_subkey_16(
171        master_key, LABEL_SM4,
172    ))));
173    registry.register(Box::new(Zuc128Encryptor::new(derive_subkey_16(
174        master_key, LABEL_ZUC,
175    ))));
176}
177
178/// 九个扩展算法(HKDF-Expand 派生)。
179fn register_extension_ciphers(
180    registry: &mut Registry<Box<dyn SymmetricCipher>>,
181    master_key: &[u8; 32],
182) {
183    registry.register(Box::new(ChaCha20Poly1305IetfEncryptor::new(
184        derive_subkey_ext(master_key, LABEL_CHACHA20_POLY1305_IETF),
185    )));
186    registry.register(Box::new(Aes256GcmSivEncryptor::new(derive_subkey_ext(
187        master_key,
188        LABEL_AES_GCM_SIV,
189    ))));
190    registry.register(Box::new(Camellia256CbcEncryptor::new(derive_subkey_ext(
191        master_key,
192        LABEL_CAMELLIA,
193    ))));
194    registry.register(Box::new(Aria256CbcEncryptor::new(derive_subkey_ext(
195        master_key, LABEL_ARIA,
196    ))));
197    registry.register(Box::new(Threefish512CbcEncryptor::new(derive_subkey_ext(
198        master_key,
199        LABEL_THREEFISH,
200    ))));
201    registry.register(Box::new(Kuznyechik256CbcEncryptor::new(derive_subkey_ext(
202        master_key,
203        LABEL_KUZNYECHIK,
204    ))));
205    registry.register(Box::new(Salsa20Encryptor::new(derive_subkey_ext(
206        master_key,
207        LABEL_SALSA20,
208    ))));
209    registry.register(Box::new(Sm4GcmEncryptor::new(derive_subkey_ext(
210        master_key,
211        LABEL_SM4_GCM,
212    ))));
213    registry.register(Box::new(Zuc256Encryptor::new(derive_subkey_ext(
214        master_key,
215        LABEL_ZUC256,
216    ))));
217}
218
219/// 旧标签子密钥 = `HMAC-SHA256(key = 主密钥, msg = 用途标签)`。
220fn derive_subkey(master_key: &[u8; 32], label: &[u8]) -> [u8; 32] {
221    let mut mac = Hmac::<Sha256>::new_from_slice(master_key).expect("HMAC accepts any key size");
222    mac.update(label);
223    mac.finalize().into_bytes().into()
224}
225
226/// SM4 / ZUC 旧标签用 16 字节子密钥:取派生结果前 16 字节(与原版一致)。
227fn derive_subkey_16(master_key: &[u8; 32], label: &[u8]) -> [u8; 16] {
228    let full = Zeroizing::new(derive_subkey(master_key, label));
229    full[..16].try_into().expect("slice is exactly 16 bytes")
230}
231
232/// 扩展子密钥 = `HKDF-Expand(PRK, info = 标签, L = N)`,其中
233/// `PRK = HMAC-SHA256(key = 主密钥, msg = [`LABEL_PRK_EXT`])`。
234fn derive_subkey_ext<const N: usize>(master_key: &[u8; 32], label: &[u8]) -> [u8; N] {
235    let prk = Zeroizing::new(derive_subkey(master_key, LABEL_PRK_EXT));
236    let hkdf = Hkdf::<Sha256>::from_prk(prk.as_slice()).expect("PRK is 32 bytes (= HashLen)");
237
238    let mut subkey = [0u8; N];
239    hkdf.expand(label, &mut subkey)
240        .expect("N in {16, 32, 64} is far below the HKDF limit of 255 * 32");
241    subkey
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    fn hex(bytes: &[u8]) -> String {
249        crate::internal::hex_encode(bytes)
250    }
251
252    #[test]
253    fn factory_validates_master_key_and_default() {
254        assert_eq!(
255            EncryptionManagerFactory::from_master_key(&[0u8; 31], "aes-256-gcm").unwrap_err(),
256            Error::InvalidMasterKey { got: 31 }
257        );
258        assert_eq!(
259            EncryptionManagerFactory::from_master_key(&[0u8; 32], "aes-512").unwrap_err(),
260            Error::UnknownIdentifier {
261                kind: "symmetric cipher",
262                identifier: "aes-512".to_string()
263            }
264        );
265    }
266
267    #[test]
268    fn factory_registers_all_five_and_they_roundtrip() {
269        let manager =
270            EncryptionManagerFactory::from_master_key(&[42u8; 32], "aes-256-gcm").unwrap();
271        let expected = [
272            "aes-256-cbc-hmac",
273            "aes-256-gcm",
274            "sm4-cbc",
275            "sodium-xchacha20",
276            "zuc-128",
277        ];
278        assert_eq!(manager.registry().identifiers(), expected);
279
280        for identifier in expected {
281            let blob = manager.encrypt_with(identifier, b"cross-check").unwrap();
282            assert_eq!(
283                manager.decrypt_with(identifier, &blob).unwrap(),
284                b"cross-check"
285            );
286        }
287    }
288
289    #[test]
290    fn subkeys_are_independent_per_algorithm() {
291        let manager =
292            EncryptionManagerFactory::from_master_key(&[42u8; 32], "aes-256-gcm").unwrap();
293        let blob = manager.encrypt_with("aes-256-gcm", b"secret").unwrap();
294        // 同一把主密钥,但换了算法:密文结构不同 / 密钥不同,解不开。
295        assert!(manager.decrypt_with("aes-256-cbc-hmac", &blob).is_err());
296        assert!(manager.decrypt_with("sm4-cbc", &blob).is_err());
297    }
298
299    /// 五个旧标签的已知答案向量:**冻结存量数据的兼容契约**。
300    ///
301    /// 常量用外部独立实现计算(Python `hmac.new(master, msg, sha256)`,
302    /// 主密钥 `[0x2a; 32]`),并与本实现的输出逐字节比对 —— 任何人改动标签
303    /// 常量或 HMAC 参数顺序,这里都会红,而存量密文不会被静默破坏。
304    #[test]
305    fn legacy_subkeys_are_frozen() {
306        let master = [42u8; 32];
307        let cases: [(&[u8], &str); 5] = [
308            (
309                LABEL_AES_GCM,
310                "8c40365a67198f254b99a3e59b5da02762b2368432e436807d52646764709efa",
311            ),
312            (
313                LABEL_AES_CBC,
314                "e2b808949cc38e4be732924c88c56738a607cbd63e1e6a81ff89cada485da4b5",
315            ),
316            (
317                LABEL_SODIUM,
318                "24f1543b4d2d3a48c835f3d10bd758e7ad94f55fed8b0fb64fa5e29fa5ff834d",
319            ),
320            (
321                LABEL_SM4,
322                "a0a1e7c2645d1eed02f2fc43eca98992009424fb80e8314b5218be6f4b831f97",
323            ),
324            (
325                LABEL_ZUC,
326                "e2ac939b959fff17dd6d7a1df705b5e00840364974c0482c30419661f4195b88",
327            ),
328        ];
329        for (label, expected) in cases {
330            assert_eq!(
331                hex(&derive_subkey(&master, label)),
332                expected,
333                "label {}",
334                String::from_utf8_lossy(label)
335            );
336        }
337        // 16 字节旧标签 = 32 字节派生结果的前 16 字节。
338        assert_eq!(
339            hex(&derive_subkey_16(&master, LABEL_SM4)),
340            "a0a1e7c2645d1eed02f2fc43eca98992"
341        );
342        assert_eq!(
343            hex(&derive_subkey_16(&master, LABEL_ZUC)),
344            "e2ac939b959fff17dd6d7a1df705b5e0"
345        );
346    }
347
348    /// 扩展域的已知答案向量(PRK + 九个子密钥),算法与旧标签同法外部计算
349    /// (Python `hmac` + RFC 5869 HKDF-Expand,后者以 RFC 5869 A.1/A.2 官方向量
350    /// 自校),再与本实现比对。改标签、改 PRK 常量、改 HKDF 参数都会红。
351    #[test]
352    fn extension_subkeys_are_frozen() {
353        let master = [42u8; 32];
354        assert_eq!(
355            hex(&derive_subkey(&master, LABEL_PRK_EXT)),
356            "d13e7885aa737fe03fd1aff45bfee04971cb42a65c6d83df6c81251b4a48e76a"
357        );
358
359        assert_eq!(
360            hex(&derive_subkey_ext::<32>(
361                &master,
362                LABEL_CHACHA20_POLY1305_IETF
363            )),
364            "b72beef1f703fbc67add7c0045129406a7e33bed2f44bf1ba5cfdbca46983f3c"
365        );
366        assert_eq!(
367            hex(&derive_subkey_ext::<32>(&master, LABEL_AES_GCM_SIV)),
368            "4ea65b0fec0a961de6c8f4a3a4e1d4a1a422d8e494ed976d51c6136ab3c65d84"
369        );
370        assert_eq!(
371            hex(&derive_subkey_ext::<32>(&master, LABEL_CAMELLIA)),
372            "64fc1e0b8ba07c51e5b3bcfc43be3d370f1d26276957d0b07144f6e3b12bbf21"
373        );
374        assert_eq!(
375            hex(&derive_subkey_ext::<32>(&master, LABEL_ARIA)),
376            "ae167985194b12afb4ee564310e23573767f06e8ac12967914b9ad651ef88b34"
377        );
378        assert_eq!(
379            hex(&derive_subkey_ext::<64>(&master, LABEL_THREEFISH)),
380            concat!(
381                "951afa424d5221c2de5f5f7f9be38e23",
382                "1eb96fc1842c25b04804c8cc50b7f131",
383                "4849ff963922f5ec93d127536f52c310",
384                "1920c5477ad5f04ad8ad122efd4c3b87"
385            )
386        );
387        assert_eq!(
388            hex(&derive_subkey_ext::<32>(&master, LABEL_KUZNYECHIK)),
389            "20825902f56afe01bc183dce96fd7d8f4a68d7e9854be51a73b9d5d1c3e3d887"
390        );
391        assert_eq!(
392            hex(&derive_subkey_ext::<32>(&master, LABEL_SALSA20)),
393            "c059a09ff49bb3c6916beb350354091b27f80c4708da3ae7d17364f1d7a6898b"
394        );
395        assert_eq!(
396            hex(&derive_subkey_ext::<16>(&master, LABEL_SM4_GCM)),
397            "3b02d8456195d02a514fdbeb5d5ff8a8"
398        );
399        assert_eq!(
400            hex(&derive_subkey_ext::<32>(&master, LABEL_ZUC256)),
401            "832ef3816ff4fde09aecce478f817d2ff878000567ffb7c7a585c21071775a4c"
402        );
403    }
404
405    /// 标签不重名 + 子密钥两两不同:一处失守就会让两个算法共用密钥。
406    #[test]
407    fn labels_and_subkeys_are_distinct() {
408        let master = [42u8; 32];
409        let legacy_labels = [
410            LABEL_AES_GCM,
411            LABEL_AES_CBC,
412            LABEL_SM4,
413            LABEL_ZUC,
414            LABEL_SODIUM,
415        ];
416        let ext_labels = [
417            LABEL_CHACHA20_POLY1305_IETF,
418            LABEL_AES_GCM_SIV,
419            LABEL_CAMELLIA,
420            LABEL_ARIA,
421            LABEL_THREEFISH,
422            LABEL_KUZNYECHIK,
423            LABEL_SALSA20,
424            LABEL_SM4_GCM,
425            LABEL_ZUC256,
426        ];
427
428        let mut labels: Vec<&[u8]> = legacy_labels.to_vec();
429        labels.extend(ext_labels.iter().copied());
430        for (i, first) in labels.iter().enumerate() {
431            for second in &labels[i + 1..] {
432                assert_ne!(first, second, "用途标签必须互不重名");
433            }
434        }
435
436        let mut subkeys: Vec<Vec<u8>> = legacy_labels
437            .iter()
438            .map(|label| derive_subkey(&master, label).to_vec())
439            .collect();
440        subkeys.extend(
441            ext_labels
442                .iter()
443                .map(|label| derive_subkey_ext::<64>(&master, label).to_vec()),
444        );
445        assert_eq!(subkeys.len(), 14);
446        for (i, first) in subkeys.iter().enumerate() {
447            for second in &subkeys[i + 1..] {
448                // 16 字节标签取前缀,与各自算法实际使用的子密钥一致。
449                assert_ne!(
450                    &first[..16],
451                    &second[..16],
452                    "任一算法的子密钥前缀都不得与其他算法相同"
453                );
454            }
455        }
456    }
457
458    /// 十四个算法全部注册且往返;默认算法校验与主密钥校验不变。
459    #[test]
460    fn all_fourteen_roundtrip_and_validate() {
461        assert_eq!(
462            EncryptionManagerFactory::from_master_key_all(&[0u8; 31], "aes-256-gcm").unwrap_err(),
463            Error::InvalidMasterKey { got: 31 }
464        );
465        assert_eq!(
466            EncryptionManagerFactory::from_master_key_all(&[0u8; 32], "aes-512").unwrap_err(),
467            Error::UnknownIdentifier {
468                kind: "symmetric cipher",
469                identifier: "aes-512".to_string()
470            }
471        );
472
473        let manager =
474            EncryptionManagerFactory::from_master_key_all(&[42u8; 32], "aes-256-gcm").unwrap();
475        let expected = [
476            "aes-256-cbc-hmac",
477            "aes-256-gcm",
478            "aes-256-gcm-siv",
479            "aria-256-cbc-hmac",
480            "camellia-256-cbc-hmac",
481            "chacha20-poly1305-ietf",
482            "kuznyechik-256-cbc-hmac",
483            "salsa20",
484            "sm4-cbc",
485            "sm4-gcm",
486            "sodium-xchacha20",
487            "threefish-512-cbc-hmac",
488            "zuc-128",
489            "zuc-256",
490        ];
491        assert_eq!(manager.registry().identifiers(), expected);
492
493        for identifier in expected {
494            let blob = manager.encrypt_with(identifier, b"cross-check").unwrap();
495            assert_eq!(
496                manager.decrypt_with(identifier, &blob).unwrap(),
497                b"cross-check",
498                "{identifier}"
499            );
500        }
501    }
502
503    /// 扩展不改变旧标签派生:两个工厂(五算法 / 十四算法)对五个默认算法的
504    /// 子密钥逐字节相同 —— 用公开 API 交叉解密验证(同子密钥才解得开)。
505    #[test]
506    fn legacy_subkeys_are_unchanged_when_extensions_are_added() {
507        let five = EncryptionManagerFactory::from_master_key(&[42u8; 32], "aes-256-gcm").unwrap();
508        let all =
509            EncryptionManagerFactory::from_master_key_all(&[42u8; 32], "aes-256-gcm").unwrap();
510
511        for identifier in five.registry().identifiers() {
512            let from_five = five.encrypt_with(identifier, b"shared subkey").unwrap();
513            assert_eq!(
514                all.decrypt_with(identifier, &from_five).unwrap(),
515                b"shared subkey",
516                "{identifier}"
517            );
518            let from_all = all.encrypt_with(identifier, b"shared subkey").unwrap();
519            assert_eq!(
520                five.decrypt_with(identifier, &from_all).unwrap(),
521                b"shared subkey",
522                "{identifier}"
523            );
524        }
525    }
526
527    /// 组合用法:先建五算法管理器,再把扩展注册进去,结果与
528    /// [`EncryptionManagerFactory::from_master_key_all`] 等价。
529    #[test]
530    fn register_extensions_composes_into_existing_registry() {
531        assert_eq!(
532            EncryptionManagerFactory::register_extensions(
533                &mut Registry::new("symmetric cipher"),
534                &[0u8; 16]
535            )
536            .unwrap_err(),
537            Error::InvalidMasterKey { got: 16 }
538        );
539
540        let mut manager =
541            EncryptionManagerFactory::from_master_key(&[42u8; 32], "aes-256-gcm").unwrap();
542        EncryptionManagerFactory::register_extensions(manager.registry_mut(), &[42u8; 32]).unwrap();
543        assert_eq!(manager.registry().identifiers().len(), 14);
544
545        let all =
546            EncryptionManagerFactory::from_master_key_all(&[42u8; 32], "aes-256-gcm").unwrap();
547        assert_eq!(
548            manager.registry().identifiers(),
549            all.registry().identifiers()
550        );
551
552        // 组合路径派生的子密钥与 from_master_key_all 一致:交叉解密。
553        for identifier in manager.registry().identifiers() {
554            let composed = manager.encrypt_with(identifier, b"composed").unwrap();
555            assert_eq!(
556                all.decrypt_with(identifier, &composed).unwrap(),
557                b"composed",
558                "{identifier}"
559            );
560        }
561    }
562}