Skip to main content

ic_canister_kit/number/
mix.rs

1//! 混合数字,利用随机填充和校验码降低连续编号的可猜测性。
2//!
3//! 该模块只提供编号混淆,不提供密码学意义上的加密或认证。
4
5use candid::CandidType;
6use serde::{Deserialize, Serialize};
7
8const CHECKSUM_LENGTH: usize = 4;
9const MAX_INDEX_LENGTH: usize = std::mem::size_of::<u64>();
10const MAX_ENCODED_LENGTH: usize = CHECKSUM_LENGTH + MAX_INDEX_LENGTH * 2;
11
12/// 根据指定序号生成一个混淆后的字节串
13#[inline]
14pub fn encode_index_code(salt: &[u8], index: u64, random: Option<&[u8]>) -> Vec<u8> {
15    let trimmed = trim_index(index); // 去除前置 0
16    let mix = mix_numbers(&trimmed, random); // 用随机数拓展位数
17
18    use sha2::Digest;
19    let mut hasher = sha2::Sha256::new();
20    hasher.update(&mix);
21    hasher.update(salt); // 加盐
22    let digest: [u8; 32] = hasher.finalize().into(); // 取得 hash 结果
23
24    let mut show = Vec::with_capacity(mix.len() + 4);
25
26    show.extend_from_slice(&digest[..CHECKSUM_LENGTH]); // 取前 4 位作为校验
27
28    show.extend_from_slice(&mix); // 补上拓展后的数据
29
30    show
31}
32
33/// 编码数字成字符串
34#[inline]
35pub fn encode_index_code_with_base32(salt: &[u8], index: u64, random: Option<&[u8]>) -> String {
36    let show = encode_index_code(salt, index, random);
37    base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &show)
38}
39
40/// 混淆错误
41#[derive(CandidType, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
42pub enum MixNumberDecodeError {
43    /// 长度错误
44    WrongLength,
45    /// 校验码错误
46    WrongChecksum,
47    /// 编码错误
48    Base32DecodeError,
49}
50impl std::fmt::Display for MixNumberDecodeError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(
53            f,
54            "{}",
55            match self {
56                MixNumberDecodeError::WrongLength => "Wrong mix number length",
57                MixNumberDecodeError::WrongChecksum => "Wrong mix number checksum",
58                MixNumberDecodeError::Base32DecodeError => "base32 decode failed",
59            }
60        )
61    }
62}
63impl std::error::Error for MixNumberDecodeError {}
64
65/// 根据加密结果解析回序号
66pub fn decode_index_code(salt: &[u8], show: &[u8]) -> Result<u64, MixNumberDecodeError> {
67    if show.len() < CHECKSUM_LENGTH + 2 || MAX_ENCODED_LENGTH < show.len() || !show.len().is_multiple_of(2) {
68        return Err(MixNumberDecodeError::WrongLength); // 长度不对
69    }
70    let mix = &show[CHECKSUM_LENGTH..];
71
72    use sha2::Digest;
73    let mut hasher = sha2::Sha256::new();
74    hasher.update(mix);
75    hasher.update(salt); // 加盐
76    let digest: [u8; 32] = hasher.finalize().into(); // 取得 hash 结果
77
78    if show[..CHECKSUM_LENGTH] != digest[..CHECKSUM_LENGTH] {
79        return Err(MixNumberDecodeError::WrongChecksum); // 校验失败
80    }
81
82    let trimmed = restore_numbers(mix)?;
83
84    let index = restore_index(&trimmed)?;
85
86    Ok(index)
87}
88/// 根据加密字符串解析回序号
89pub fn decode_index_code_by_base32(salt: &[u8], code: &str) -> Result<u64, MixNumberDecodeError> {
90    let show = base32::decode(base32::Alphabet::Rfc4648 { padding: false }, code)
91        .ok_or(MixNumberDecodeError::Base32DecodeError)?;
92    decode_index_code(salt, &show)
93}
94
95// ================ 工具方法 ================
96
97// 裁剪数字
98// 保留有效位的数字, 最少一个 u8 // ? 也就是说前面太多 0 的情况下会只留下后面有效的
99fn trim_index(index: u64) -> Vec<u8> {
100    let bytes = index.to_be_bytes();
101    let trimmed: Vec<u8> = bytes.into_iter().skip_while(|n| *n == 0).collect();
102    if trimmed.is_empty() { vec![0] } else { trimmed }
103}
104
105// 恢复数字 大端法 高位在前
106#[allow(clippy::identity_op)]
107fn restore_index(numbers: &[u8]) -> Result<u64, MixNumberDecodeError> {
108    if numbers.is_empty() || MAX_INDEX_LENGTH < numbers.len() {
109        return Err(MixNumberDecodeError::WrongLength);
110    }
111
112    let mut bytes = [0_u8; 8];
113
114    let len = numbers.len();
115    for i in 0..len {
116        bytes[8 - len + i] = numbers[i];
117    }
118
119    Ok(u64::from_be_bytes(bytes))
120}
121
122// 混合数字 // 位数交叉
123fn mix_numbers(numbers: &[u8], random: Option<&[u8]>) -> Vec<u8> {
124    #[allow(clippy::identity_op)]
125    fn mix_single(m: u8, n: u8) -> [u8; 2] {
126        [
127            0b0000_0000
128                | ((m & 0b1000_0000) >> 0)  // x000_0000 奇数位
129                | ((m & 0b0100_0000) >> 1)  // 00x0_0000 奇数位
130                | ((m & 0b0010_0000) >> 2)  // 0000_x000 奇数位
131                | ((m & 0b0001_0000) >> 3)  // 0000_00x0 奇数位
132                | ((n & 0b1000_0000) >> 1)  // 0x00_0000 偶数位
133                | ((n & 0b0100_0000) >> 2)  // 000x_0000 偶数位
134                | ((n & 0b0010_0000) >> 3)  // 0000_0x00 偶数位
135                | ((n & 0b0001_0000) >> 4), // 0000_000x 偶数位
136            0b0000_0000
137                | ((m & 0b0000_1000) << 4)  // x000_0000 奇数位
138                | ((m & 0b0000_0100) << 3)  // 00x0_0000 奇数位
139                | ((m & 0b0000_0010) << 2)  // 0000_x000 奇数位
140                | ((m & 0b0000_0001) << 1)  // 0000_00x0 奇数位
141                | ((n & 0b0000_1000) << 3)  // 0x00_0000 偶数位
142                | ((n & 0b0000_0100) << 2)  // 000x_0000 偶数位
143                | ((n & 0b0000_0010) << 1)  // 0000_0x00 偶数位
144                | ((n & 0b0000_0001) << 0), // 0000_000x 偶数位
145        ]
146    }
147
148    let random = random.unwrap_or_default();
149
150    let mut ns = Vec::with_capacity(numbers.len() * 2);
151    for (i, n) in numbers.iter().enumerate() {
152        ns.extend_from_slice(&mix_single(random.get(i).copied().unwrap_or(0), *n));
153    }
154    ns
155}
156
157// 恢复数字 // 位数交叉
158fn restore_numbers(ns: &[u8]) -> Result<Vec<u8>, MixNumberDecodeError> {
159    #[allow(clippy::identity_op)]
160    fn restore_single(n1: u8, n2: u8) -> u8 {
161        0b0000_0000
162            | ((n1 & 0b0100_0000) << 1)
163            | ((n1 & 0b0001_0000) << 2)
164            | ((n1 & 0b0000_0100) << 3)
165            | ((n1 & 0b0000_0001) << 4)
166            | ((n2 & 0b0100_0000) >> 3)
167            | ((n2 & 0b0001_0000) >> 2)
168            | ((n2 & 0b0000_0100) >> 1)
169            | ((n2 & 0b0000_0001) >> 0)
170    }
171
172    if ns.is_empty() || !ns.len().is_multiple_of(2) || MAX_INDEX_LENGTH * 2 < ns.len() {
173        return Err(MixNumberDecodeError::WrongLength);
174    }
175
176    let mut numbers = Vec::new();
177    for i in 0..(ns.len() / 2) {
178        numbers.push(restore_single(ns[i * 2], ns[i * 2 + 1]));
179    }
180    Ok(numbers)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::{MixNumberDecodeError, decode_index_code, decode_index_code_by_base32, encode_index_code};
186
187    #[test]
188    fn round_trips_boundary_values() {
189        let salt = b"private-salt";
190        for value in [0, 1, 255, 256, u64::MAX] {
191            let encoded = encode_index_code(salt, value, Some(b"random!!"));
192            assert_eq!(decode_index_code(salt, &encoded), Ok(value));
193        }
194    }
195
196    #[test]
197    fn rejects_invalid_checksum_and_oversized_payload() {
198        let salt = b"private-salt";
199        let mut encoded = encode_index_code(salt, 42, None);
200        encoded[0] ^= 1;
201        assert!(matches!(
202            decode_index_code(salt, &encoded),
203            Err(MixNumberDecodeError::WrongChecksum)
204        ));
205
206        assert!(matches!(
207            decode_index_code(salt, &[0; 22]),
208            Err(MixNumberDecodeError::WrongLength)
209        ));
210    }
211
212    #[test]
213    fn rejects_invalid_base32_without_panicking() {
214        assert!(matches!(
215            decode_index_code_by_base32(b"salt", "***"),
216            Err(MixNumberDecodeError::Base32DecodeError)
217        ));
218    }
219}