use aes::Aes128;
use fpe::ff1::{FF1, FlexibleNumeralString};
use crate::{TnidVariant, utils};
const FF1_RADIX: u32 = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncryptionKeyError {
WrongHexLength(usize),
InvalidHexChar {
position: usize,
character: char,
},
WrongByteLength(usize),
NonAscii,
}
impl std::fmt::Display for EncryptionKeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WrongHexLength(len) => {
write!(
f,
"encryption key hex string must be 32 characters, got {len}"
)
}
Self::InvalidHexChar {
position,
character,
} => {
write!(
f,
"invalid hex character '{}' (U+{:04X}) at position {position}",
character, *character as u32
)
}
Self::WrongByteLength(len) => {
write!(f, "encryption key slice must be 16 bytes, got {len}")
}
Self::NonAscii => {
write!(f, "encryption key hex string must be ASCII")
}
}
}
}
impl std::error::Error for EncryptionKeyError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncryptionError {
UnsupportedVariant(TnidVariant),
}
impl std::fmt::Display for EncryptionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedVariant(variant) => {
write!(
f,
"TNID variant {variant:?} is not supported for encryption/decryption"
)
}
}
}
}
impl std::error::Error for EncryptionError {}
pub struct EncryptionKey(FF1<Aes128>);
impl EncryptionKey {
pub fn new(bytes: [u8; 16]) -> Self {
Self(FF1::<Aes128>::new(&bytes, FF1_RADIX).expect("radix 16 is always valid"))
}
pub fn from_hex(s: &str) -> Result<Self, EncryptionKeyError> {
if s.len() != 32 {
return Err(EncryptionKeyError::WrongHexLength(s.len()));
}
if !s.is_ascii() {
return Err(EncryptionKeyError::NonAscii);
}
let bytes_slice = s.as_bytes();
let mut bytes = [0u8; 16];
for (i, chunk) in bytes_slice.chunks(2).enumerate() {
let pos = i * 2;
let high_byte = *chunk.first().expect("hex chunk should have first byte");
let low_byte = *chunk.get(1).expect("hex chunk should have second byte");
let high_char = high_byte as char;
let low_char = low_byte as char;
let high =
utils::hex_char_to_nibble(high_byte).ok_or(EncryptionKeyError::InvalidHexChar {
position: pos,
character: high_char,
})?;
let low =
utils::hex_char_to_nibble(low_byte).ok_or(EncryptionKeyError::InvalidHexChar {
position: pos + 1,
character: low_char,
})?;
let byte_slot = bytes
.get_mut(i)
.expect("hex chunk index must fit into output buffer");
*byte_slot = (high << 4) | low;
}
Ok(Self::new(bytes))
}
pub fn from_slice(s: &[u8]) -> Result<Self, EncryptionKeyError> {
let bytes: [u8; 16] = s
.try_into()
.map_err(|_| EncryptionKeyError::WrongByteLength(s.len()))?;
Ok(Self::new(bytes))
}
}
pub const RIGHT_SECRET_DATA_SECTION_MASK: u128 = 0x00000000_0000_0000_0fff_ffffffffffff;
pub const MIDDLE_SECRET_DATA_SECTION_MASK: u128 = 0x00000000_0000_0fff_0000_000000000000;
pub const LEFT_SECRET_DATA_SECTION_MASK: u128 = 0x00000fff_ffff_0000_0000_000000000000;
pub const COMPLETE_SECRET_DATA_MASK: u128 = RIGHT_SECRET_DATA_SECTION_MASK
| MIDDLE_SECRET_DATA_SECTION_MASK
| LEFT_SECRET_DATA_SECTION_MASK;
pub fn extract_secret_data_bits(id: u128) -> u128 {
let extracted = id & RIGHT_SECRET_DATA_SECTION_MASK;
const BETWEEN_MIDDLE_RIGHT: i32 = 4;
let extracted = extracted | ((id & MIDDLE_SECRET_DATA_SECTION_MASK) >> BETWEEN_MIDDLE_RIGHT);
const BETWEEN_LEFT_MIDDLE: i32 = BETWEEN_MIDDLE_RIGHT + 4;
extracted | ((id & LEFT_SECRET_DATA_SECTION_MASK) >> BETWEEN_LEFT_MIDDLE)
}
pub fn expand_secret_data_bits(bits: u128) -> u128 {
let expanded = bits & RIGHT_SECRET_DATA_SECTION_MASK;
const BETWEEN_MIDDLE_RIGHT: i32 = 4;
let middle_mask = MIDDLE_SECRET_DATA_SECTION_MASK >> BETWEEN_MIDDLE_RIGHT;
let expanded = expanded | ((bits & middle_mask) << BETWEEN_MIDDLE_RIGHT);
const BETWEEN_LEFT_MIDDLE: i32 = BETWEEN_MIDDLE_RIGHT + 4;
let left_mask = LEFT_SECRET_DATA_SECTION_MASK >> BETWEEN_LEFT_MIDDLE;
expanded | ((bits & left_mask) << BETWEEN_LEFT_MIDDLE)
}
const SECRET_DATA_BIT_NUM: u8 = COMPLETE_SECRET_DATA_MASK.count_ones() as u8;
const HEX_DIGIT_COUNT: usize = 25;
fn u128_to_hex_digits(data: u128) -> Vec<u16> {
let mut hex_digits = Vec::with_capacity(HEX_DIGIT_COUNT);
for i in 0..HEX_DIGIT_COUNT {
let shift = (HEX_DIGIT_COUNT - 1 - i) * 4;
hex_digits.push(((data >> shift) & 0xF) as u16);
}
hex_digits
}
fn hex_digits_to_u128(digits: &[u16]) -> u128 {
let mut result = 0u128;
for digit in digits {
result = (result << 4) | (*digit as u128);
}
result
}
pub fn encrypt(id_secret_data: u128, key: &EncryptionKey) -> u128 {
let mask = (1u128 << SECRET_DATA_BIT_NUM) - 1;
let data = id_secret_data & mask;
let hex_digits = u128_to_hex_digits(data);
let numeral_string = FlexibleNumeralString::from(hex_digits);
let encrypted = key
.0
.encrypt(&[], &numeral_string)
.expect("string is in required radix");
let encrypted_digits: Vec<u16> = encrypted.into();
hex_digits_to_u128(&encrypted_digits)
}
pub fn decrypt(id_secret_data: u128, key: &EncryptionKey) -> u128 {
let mask = (1u128 << SECRET_DATA_BIT_NUM) - 1;
let data = id_secret_data & mask;
let hex_digits = u128_to_hex_digits(data);
let numeral_string = FlexibleNumeralString::from(hex_digits);
let decrypted = key
.0
.decrypt(&[], &numeral_string)
.expect("string is in required radix");
let decrypted_digits: Vec<u16> = decrypted.into();
hex_digits_to_u128(&decrypted_digits)
}
pub fn encrypt_id_v0_to_v1(id: u128, key: &EncryptionKey) -> Result<u128, EncryptionError> {
match TnidVariant::from_id(id) {
TnidVariant::V0 => {}
TnidVariant::V1 => return Ok(id),
variant @ (TnidVariant::V2 | TnidVariant::V3) => {
return Err(EncryptionError::UnsupportedVariant(variant));
}
}
let secret_data = extract_secret_data_bits(id);
let encrypted_data = encrypt(secret_data, key);
let expanded = expand_secret_data_bits(encrypted_data);
let id = (id & !COMPLETE_SECRET_DATA_MASK) | expanded;
let id = utils::change_variant(id, TnidVariant::V1);
Ok(id)
}
pub fn decrypt_id_v1_to_v0(id: u128, key: &EncryptionKey) -> Result<u128, EncryptionError> {
match TnidVariant::from_id(id) {
TnidVariant::V0 => return Ok(id),
TnidVariant::V1 => {}
variant @ (TnidVariant::V2 | TnidVariant::V3) => {
return Err(EncryptionError::UnsupportedVariant(variant));
}
}
let encrypted_data = extract_secret_data_bits(id);
let decrypted_data = decrypt(encrypted_data, key);
let expanded = expand_secret_data_bits(decrypted_data);
let id = (id & !COMPLETE_SECRET_DATA_MASK) | expanded;
let id = utils::change_variant(id, TnidVariant::V0);
Ok(id)
}
#[cfg(all(test, not(debug_assertions)))]
mod tests_release {
use super::*;
use proptest::prelude::*;
use proptest::test_runner::TestRunner;
#[test]
fn decrypt_no_panic() {
let mut runner = TestRunner::new(ProptestConfig {
cases: 100_000,
..ProptestConfig::default()
});
runner
.run(
&(any::<u128>(), any::<u128>()),
|(id_secret_data, secret)| {
let key = EncryptionKey::new(secret.to_le_bytes());
decrypt(id_secret_data, &key);
Ok(())
},
)
.unwrap();
}
fn count_ones_at_bit(samples: &[u128], bit_pos: u32) -> usize {
samples.iter().filter(|x| (*x >> bit_pos) & 1 == 1).count()
}
const TEST_KEY_BYTES: [u8; 16] = [
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f,
0x3c,
];
fn test_key() -> EncryptionKey {
EncryptionKey::new(TEST_KEY_BYTES)
}
#[test]
fn bit_frequency_is_uniform() {
let key = test_key();
const SAMPLE_COUNT: usize = 50_000;
const SECRET_BITS: u32 = SECRET_DATA_BIT_NUM as u32;
let base_time: u64 = 1_750_000_000_000; let encrypted_samples: Vec<u128> = (0..SAMPLE_COUNT)
.map(|i| {
let timestamp = base_time + (i as u64);
let random = (i as u64).wrapping_mul(0x123456789ABCDEF);
simulate_v0_secret_data(timestamp, random)
})
.map(|plaintext| encrypt(plaintext, &key))
.collect();
for bit_pos in 0..SECRET_BITS {
let ones = count_ones_at_bit(&encrypted_samples, bit_pos);
let ratio = ones as f64 / SAMPLE_COUNT as f64;
assert!(
(ratio - 0.5).abs() < 0.02,
"Bit {} has biased frequency: {:.2}% ones (expected ~50%)",
bit_pos,
ratio * 100.0
);
}
}
#[test]
fn sequential_timestamps_produce_random_ordering() {
let key = test_key();
const SAMPLE_COUNT: usize = 10_000;
let base_time: u64 = 1_750_000_000_000;
let encrypted: Vec<u128> = (0..SAMPLE_COUNT)
.map(|i| {
let timestamp = base_time + (i as u64);
let random = 0x123456789ABCDEF0_u64; simulate_v0_secret_data(timestamp, random)
})
.map(|plaintext| encrypt(plaintext, &key))
.collect();
let mut greater_count = 0;
for i in 1..encrypted.len() {
if encrypted[i] > encrypted[i - 1] {
greater_count += 1;
}
}
let ratio = greater_count as f64 / (SAMPLE_COUNT - 1) as f64;
assert!(
(ratio - 0.5).abs() < 0.02,
"Sequential timestamps produce ordered outputs: {:.2}% ascending (expected ~50%)",
ratio * 100.0
);
}
#[test]
fn avalanche_effect_single_bit_flip() {
let key = test_key();
const SAMPLE_COUNT: usize = 10_000;
const SECRET_BITS: u32 = SECRET_DATA_BIT_NUM as u32;
let mut total_flipped_bits = 0u64;
let mut min_flipped = u32::MAX;
let mut max_flipped = 0u32;
for i in 0..SAMPLE_COUNT {
let input1 = (i as u128).wrapping_mul(0x123456789ABCDEF0123456789ABCDEF)
& ((1u128 << SECRET_BITS) - 1);
let bit_to_flip = (i as u32 * 7) % SECRET_BITS;
let input2 = input1 ^ (1u128 << bit_to_flip);
let output1 = encrypt(input1, &key);
let output2 = encrypt(input2, &key);
let diff = (output1 ^ output2).count_ones();
total_flipped_bits += diff as u64;
min_flipped = min_flipped.min(diff);
max_flipped = max_flipped.max(diff);
}
let avg_flipped = total_flipped_bits as f64 / SAMPLE_COUNT as f64;
let expected = SECRET_BITS as f64 / 2.0;
assert!(
(avg_flipped - expected).abs() < 5.0,
"Avalanche effect too weak: avg {:.1} bits flipped (expected ~{:.0})",
avg_flipped,
expected
);
assert!(
min_flipped >= 25,
"Minimum bit flips too low: {} (suggests weak diffusion)",
min_flipped
);
assert!(
max_flipped <= 75,
"Maximum bit flips too high: {} (suggests weak diffusion)",
max_flipped
);
}
#[test]
fn first_nibble_uniformly_distributed() {
let key = test_key();
const SAMPLE_COUNT: usize = 50_000;
const BUCKETS: usize = 16;
let base_time: u64 = 1_750_000_000_000;
let mut counts = [0usize; BUCKETS];
for i in 0..SAMPLE_COUNT {
let timestamp = base_time + (i as u64);
let random = (i as u64).wrapping_mul(0xDEADBEEFCAFEBABE);
let plaintext = simulate_v0_secret_data(timestamp, random);
let encrypted = encrypt(plaintext, &key);
let first_nibble = ((encrypted >> 96) & 0xF) as usize;
counts[first_nibble] += 1;
}
let expected_per_bucket = SAMPLE_COUNT / BUCKETS;
for (nibble, &count) in counts.iter().enumerate() {
let deviation =
(count as f64 - expected_per_bucket as f64).abs() / expected_per_bucket as f64;
assert!(
deviation < 0.20,
"First nibble {} has uneven distribution: {} occurrences (expected ~{}, deviation {:.1}%)",
nibble,
count,
expected_per_bucket,
deviation * 100.0
);
}
}
#[test]
fn no_collisions_in_encrypted_output() {
let key = test_key();
const SAMPLE_COUNT: usize = 100_000;
let base_time: u64 = 1_750_000_000_000;
let encrypted: std::collections::HashSet<u128> = (0..SAMPLE_COUNT)
.map(|i| {
let timestamp = base_time + (i as u64);
let random = i as u64;
simulate_v0_secret_data(timestamp, random)
})
.map(|plaintext| encrypt(plaintext, &key))
.collect();
assert_eq!(
encrypted.len(),
SAMPLE_COUNT,
"Collision detected: {} unique outputs from {} inputs",
encrypted.len(),
SAMPLE_COUNT
);
}
#[test]
fn different_keys_produce_unrelated_outputs() {
let key1 = test_key();
let mut key2_bytes = TEST_KEY_BYTES;
key2_bytes[0] ^= 1; let key2 = EncryptionKey::new(key2_bytes);
const SAMPLE_COUNT: usize = 1_000;
const SECRET_BITS: u32 = SECRET_DATA_BIT_NUM as u32;
let mut total_diff_bits = 0u64;
for i in 0..SAMPLE_COUNT {
let plaintext = (i as u128).wrapping_mul(0x123456789) & ((1u128 << SECRET_BITS) - 1);
let encrypted1 = encrypt(plaintext, &key1);
let encrypted2 = encrypt(plaintext, &key2);
total_diff_bits += (encrypted1 ^ encrypted2).count_ones() as u64;
}
let avg_diff = total_diff_bits as f64 / SAMPLE_COUNT as f64;
let expected = SECRET_BITS as f64 / 2.0;
assert!(
(avg_diff - expected).abs() < 5.0,
"Key sensitivity too low: avg {:.1} bits differ (expected ~{:.0})",
avg_diff,
expected
);
}
#[test]
fn byte_distribution_uniform() {
let key = test_key();
const SAMPLE_COUNT: usize = 25_600; const BYTES_TO_CHECK: usize = 4;
let base_time: u64 = 1_750_000_000_000;
let mut byte_counts: [[usize; 256]; BYTES_TO_CHECK] = [[0; 256]; BYTES_TO_CHECK];
for i in 0..SAMPLE_COUNT {
let timestamp = base_time + (i as u64);
let random = (i as u64).wrapping_mul(0xFEDCBA9876543210);
let plaintext = simulate_v0_secret_data(timestamp, random);
let encrypted = encrypt(plaintext, &key);
for byte_idx in 0..BYTES_TO_CHECK {
let shift = byte_idx * 8; let byte_val = ((encrypted >> shift) & 0xFF) as usize;
byte_counts[byte_idx][byte_val] += 1;
}
}
let expected_per_value = SAMPLE_COUNT / 256;
for (byte_idx, counts) in byte_counts.iter().enumerate() {
let max_deviation = counts
.iter()
.map(|&c| {
((c as f64) - (expected_per_value as f64)).abs() / (expected_per_value as f64)
})
.fold(0.0f64, f64::max);
assert!(
max_deviation < 0.50,
"Byte {} has non-uniform distribution (max deviation {:.1}%)",
byte_idx,
max_deviation * 100.0
);
}
}
#[test]
fn run_lengths_are_reasonable() {
let key = test_key();
const SAMPLE_COUNT: usize = 1_000;
const SECRET_BITS: u32 = SECRET_DATA_BIT_NUM as u32;
let mut total_runs = 0u64;
for i in 0..SAMPLE_COUNT {
let timestamp = 1_750_000_000_000u64 + (i as u64);
let random = (i as u64).wrapping_mul(0xABCDEF0123456789);
let plaintext = simulate_v0_secret_data(timestamp, random);
let encrypted = encrypt(plaintext, &key);
let mut runs = 1u32;
for bit_pos in 1..SECRET_BITS {
let prev_bit = (encrypted >> (bit_pos - 1)) & 1;
let curr_bit = (encrypted >> bit_pos) & 1;
if prev_bit != curr_bit {
runs += 1;
}
}
total_runs += runs as u64;
}
let avg_runs_per_sample = total_runs as f64 / SAMPLE_COUNT as f64;
let expected_runs = (SECRET_BITS as f64 + 1.0) / 2.0;
assert!(
(avg_runs_per_sample - expected_runs).abs() < 5.0,
"Unusual run pattern: avg {:.1} runs per sample (expected ~{:.1})",
avg_runs_per_sample,
expected_runs
);
}
fn simulate_v0_secret_data(epoch_millis: u64, random: u64) -> u128 {
let timestamp_bits = (epoch_millis & ((1u64 << 43) - 1)) as u128;
let random_bits = (random & ((1u64 << 57) - 1)) as u128;
(timestamp_bits << 57) | random_bits
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn secret_data_extract_correctly() {
let extract = extract_secret_data_bits(u128::MAX);
assert_eq!(extract.leading_zeros(), 28);
assert_eq!(extract.count_ones(), SECRET_DATA_BIT_NUM as u32);
assert_eq!(
COMPLETE_SECRET_DATA_MASK.count_ones(),
SECRET_DATA_BIT_NUM as u32
);
let extract = extract_secret_data_bits(COMPLETE_SECRET_DATA_MASK);
assert_eq!(extract.leading_zeros(), 28);
assert_eq!(extract.count_ones(), SECRET_DATA_BIT_NUM as u32);
}
#[test]
fn secret_data_expand_correctly() {
let expanded = expand_secret_data_bits(u128::MAX);
assert_eq!(expanded, COMPLETE_SECRET_DATA_MASK);
assert_eq!(expanded.count_ones(), SECRET_DATA_BIT_NUM as u32);
}
#[test]
fn secret_data_roundtrip() {
let original = COMPLETE_SECRET_DATA_MASK;
let extracted = extract_secret_data_bits(original);
let expanded = expand_secret_data_bits(extracted);
assert_eq!(expanded, original);
let pattern = 0x00000aaa_aaaa_0000_0555_555555555555u128;
let extracted = extract_secret_data_bits(pattern);
let expanded = expand_secret_data_bits(extracted);
assert_eq!(
expanded & COMPLETE_SECRET_DATA_MASK,
pattern & COMPLETE_SECRET_DATA_MASK
);
}
#[test]
fn encryption_round_trip() {
let key = EncryptionKey::new([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
let id_secret_data = extract_secret_data_bits(u128::MAX);
let encrypted = encrypt(id_secret_data, &key);
let decrypted = decrypt(encrypted, &key);
dbg!(id_secret_data, encrypted, decrypted);
assert_eq!(decrypted, id_secret_data);
}
}