pub mod names;
pub mod short_text;
use chacha20::ChaCha20;
use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
use hkdf::Hkdf;
use hmac::{Hmac, Mac};
use sha2::Sha256;
pub const SECRET_LEN: usize = 16;
pub const KEY_LEN: usize = 32;
pub const NONCE_LEN: usize = 12;
pub fn gen_secret() -> [u8; SECRET_LEN] {
let mut s = [0u8; SECRET_LEN];
getrandom::fill(&mut s).expect("系统随机数不可用");
s
}
pub fn derive_root_key(password: &[u8]) -> [u8; SECRET_LEN] {
let mut fk = [0u8; SECRET_LEN];
derive(password, &[], "sd.root.key", &mut fk);
fk
}
fn derive(pw: &[u8], salt: &[u8], info: &str, out: &mut [u8]) {
let hk = Hkdf::<Sha256>::new(Some(salt), pw);
hk.expand(info.as_bytes(), out).expect("HKDF 输出长度非法");
}
pub fn content_cipher_params(pw: &[u8]) -> ([u8; KEY_LEN], [u8; NONCE_LEN]) {
let mut key = [0u8; KEY_LEN];
let mut nonce = [0u8; NONCE_LEN];
derive(pw, &[], "sd.content.key", &mut key);
derive(pw, &[], "sd.content.nonce", &mut nonce);
(key, nonce)
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn apply_content_keystream(pw: &[u8], merged_offset: u64, data: &mut [u8]) {
if data.is_empty() {
return;
}
let (key, nonce) = content_cipher_params(pw);
let mut cipher = ChaCha20::new(&key.into(), &nonce.into());
cipher
.try_seek(merged_offset)
.expect("ChaCha20 seek 超出 keystream 范围");
cipher.apply_keystream(data);
}
pub fn name_cipher_key(pw: &[u8], salt: &[u8]) -> [u8; KEY_LEN] {
let mut key = [0u8; KEY_LEN];
derive(pw, salt, "sd.name.key", &mut key);
key
}
pub fn name_mac_key(pw: &[u8]) -> [u8; KEY_LEN] {
let mut key = [0u8; KEY_LEN];
derive(pw, &[], "sd.name.mac", &mut key);
key
}
pub struct ChunkPrp {
key: [u8; KEY_LEN],
}
const FEISTEL_ROUNDS: u8 = 4;
impl ChunkPrp {
pub fn new(pw: &[u8]) -> Self {
let mut key = [0u8; KEY_LEN];
derive(pw, &[], "sd.chunk.prp", &mut key);
Self { key }
}
fn width_bytes(index: usize) -> usize {
let mut bytes = 1usize;
let mut cap = 256u64;
while index as u64 >= cap {
bytes += 1;
cap = cap.saturating_mul(256);
}
bytes
}
fn round_f(&self, w: usize, round: u8, half: u64) -> u64 {
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(&self.key).expect("HMAC 任意密钥长");
mac.update(&[w as u8, round]);
mac.update(&half.to_be_bytes());
let out = mac.finalize().into_bytes();
u64::from_be_bytes(out[..8].try_into().expect("SHA256 输出 ≥8 字节"))
}
fn permute(&self, w: usize, v: u64, inverse: bool) -> u64 {
let half_bits = (4 * w) as u32;
let mask = (1u64 << half_bits) - 1;
let (mut l, mut r) = (v >> half_bits, v & mask);
if !inverse {
for round in 0..FEISTEL_ROUNDS {
(l, r) = (r, l ^ (self.round_f(w, round, r) & mask));
}
} else {
for round in (0..FEISTEL_ROUNDS).rev() {
(l, r) = (r ^ (self.round_f(w, round, l) & mask), l);
}
}
l << half_bits | r
}
pub fn name_of(&self, index: usize) -> String {
let w = Self::width_bytes(index);
let v = self.permute(w, index as u64, false);
let bytes = v.to_be_bytes();
let mut name = String::with_capacity(w * 2);
for b in &bytes[8 - w..] {
name.push_str(&format!("{b:02x}"));
}
name
}
pub fn index_of(&self, name: &str) -> Option<usize> {
let n = name.len();
if n == 0 || !n.is_multiple_of(2) || n > 16 {
return None;
}
let w = n / 2;
if !name
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
return None;
}
let v = u64::from_str_radix(name, 16).ok()?;
let index = self.permute(w, v, true) as usize;
(Self::width_bytes(index) == w).then_some(index)
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn chunk_count(size: u64, volume_size: u64) -> usize {
if size == 0 {
1
} else {
size.div_ceil(volume_size) as usize
}
}
pub fn gen_chunk_names(pw: &[u8], count: usize) -> Vec<String> {
let prp = ChunkPrp::new(pw);
(0..count).map(|i| prp.name_of(i)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keystream_roundtrip_and_offset_addressing() {
let pw = gen_secret();
let plain: Vec<u8> = (0..100_000u32).map(|i| (i % 251) as u8).collect();
let mut ct = plain.clone();
apply_content_keystream(&pw, 0, &mut ct);
assert_ne!(ct, plain);
assert_eq!(ct.len(), plain.len(), "流密码:密文长 = 明文长");
let mut back = ct.clone();
apply_content_keystream(&pw, 0, &mut back);
assert_eq!(back, plain);
for (start, len) in [
(0usize, 1usize),
(63, 2),
(64, 64),
(12345, 7000),
(99_999, 1),
] {
let mut piece = ct[start..start + len].to_vec();
apply_content_keystream(&pw, start as u64, &mut piece);
assert_eq!(
piece,
&plain[start..start + len],
"offset={start} len={len}"
);
}
}
#[test]
fn different_passwords_differ() {
let (a, b) = (gen_secret(), gen_secret());
let mut x = vec![0u8; 64];
let mut y = vec![0u8; 64];
apply_content_keystream(&a, 0, &mut x);
apply_content_keystream(&b, 0, &mut y);
assert_ne!(x, y);
}
#[test]
fn split_anywhere_decrypts_like_hydraria_volumes() {
let pw = gen_secret();
let plain: Vec<u8> = (0..50_000u32).map(|i| (i * 7 % 256) as u8).collect();
let mut ct = plain.clone();
apply_content_keystream(&pw, 0, &mut ct);
let cuts = [0usize, 100, 4096, 17_000, 50_000];
let mut restored = Vec::new();
for w in cuts.windows(2) {
let (s, e) = (w[0], w[1]);
let mut vol = ct[s..e].to_vec();
apply_content_keystream(&pw, s as u64, &mut vol);
restored.extend_from_slice(&vol);
}
assert_eq!(restored, plain);
}
#[test]
fn chunk_prp_bijective_and_deterministic() {
let pw = gen_secret();
let prp = ChunkPrp::new(&pw);
let all: std::collections::HashSet<String> = (0..256).map(|i| prp.name_of(i)).collect();
assert_eq!(all.len(), 256, "1 字节域必须是完整置换");
for i in 0..256 {
assert_eq!(prp.index_of(&prp.name_of(i)), Some(i), "卷 {i} 反解");
}
for i in [0usize, 255, 256, 65_535, 65_536, 4_000_000] {
let name = prp.name_of(i);
let expect_len = if i < 256 {
2
} else if i < 65_536 {
4
} else {
6
};
assert_eq!(name.len(), expect_len, "卷 {i}: {name}");
assert!(
name.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
);
assert_eq!(prp.index_of(&name), Some(i));
assert_eq!(ChunkPrp::new(&pw).name_of(i), name, "同密码可再生成");
}
assert!(prp.index_of("").is_none());
assert!(prp.index_of("abc").is_none(), "奇数长度");
assert!(prp.index_of("AB").is_none(), "大写");
assert!(prp.index_of("zz").is_none(), "非 hex");
let short_idx = (0..256).map(|i| prp.name_of(i)).next().unwrap();
assert!(prp.index_of(&format!("{short_idx}{short_idx}")).is_none() || true);
assert_ne!(gen_chunk_names(&gen_secret(), 10), gen_chunk_names(&pw, 10));
assert_eq!(
gen_chunk_names(&pw, 5),
(0..5).map(|i| prp.name_of(i)).collect::<Vec<_>>()
);
}
#[test]
fn chunk_count_boundaries() {
assert_eq!(chunk_count(0, 100), 1);
assert_eq!(chunk_count(1, 100), 1);
assert_eq!(chunk_count(100, 100), 1);
assert_eq!(chunk_count(101, 100), 2);
}
}