feat: add secret_int macro

This commit is contained in:
Kristofers Solo 2025-10-13 19:01:52 +03:00
parent 27b31d1fcc
commit e75eff8cd5
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
9 changed files with 366 additions and 1 deletions

View File

@ -11,6 +11,7 @@ des = { path = "des" }
rand = "0.9"
rstest = "0.26"
thiserror = "2"
zeroize = { version = "1.8", features = ["derive"] }
[workspace.lints.clippy]
pedantic = "warn"

View File

@ -28,7 +28,7 @@ pub trait BlockLike: Sized + Copy + Clone {
/// Minimal trait describing a fixed-size key-like value.
///
/// Concrete key types (e.g. `Secret<const N: usize>`) should implement this.
pub trait KeyLike: Sized + Copy + Clone {
pub trait KeyLike: Sized {
/// Size of the key in bytes.
const SIZE: usize;

View File

@ -5,6 +5,8 @@ authors = ["Kristofers Solo <dev@kristofers.xyz>"]
edition = "2024"
[dependencies]
cipher-core.workspace = true
zeroize.workspace = true
[dev-dependencies]
claims.workspace = true

86
des/src/constants.rs Normal file
View File

@ -0,0 +1,86 @@
// DES Constants (from FIPS 46-3 spec)
/// Initial Permutation (IP) table.
pub const IP: [u8; 64] = [
58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6,
64, 56, 48, 40, 32, 24, 16, 8, 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61,
53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7,
];
/// Inverse Initial Permutation (FP) table.
pub const FP: [u8; 64] = [
40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30,
37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25,
];
/// Expansion permutation (32 to 48 bits).
pub const E_BOX: [u8; 48] = [
32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17, 16, 17, 18,
19, 20, 21, 20, 21, 22, 23, 24, 25, 24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1,
];
/// P-box permutation (32 bits).
pub const P_BOX: [u8; 32] = [
16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10, 2, 8, 24, 14, 32, 27, 3, 9, 19,
13, 30, 6, 22, 11, 4, 25,
];
/// S-boxes: 8 boxes, each 4x16 (row x col) -> 0-15.
pub const S_BOXES: [[[u8; 16]; 4]; 8] = [
// S1
[
[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7],
[0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8],
[4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0],
[15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13],
],
// S2
[
[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10],
[3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5],
[0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15],
[13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9],
],
// S3
[
[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8],
[13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1],
[13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7],
[1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12],
],
// S4
[
[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15],
[13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9],
[10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4],
[3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14],
],
// S5
[
[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9],
[14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6],
[4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14],
[11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3],
],
// S6
[
[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11],
[10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8],
[9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6],
[4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13],
],
// S7
[
[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1],
[13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6],
[1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2],
[6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12],
],
// S8
[
[13, 2, 8, 4, 6, 15, 11, 1, 19, 9, 3, 14, 5, 0, 12, 7],
[1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2],
[7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8],
[2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],
],
];

14
des/src/des.rs Normal file
View File

@ -0,0 +1,14 @@
use crate::key::subkeys::Subkeys;
use cipher_core::KeyLike;
#[derive(Debug)]
pub struct Des {
subkeys: Subkeys,
}
impl Des {
#[must_use]
pub fn new(_key: impl KeyLike) -> Self {
todo!()
}
}

19
des/src/key/mod.rs Normal file
View File

@ -0,0 +1,19 @@
mod secret_int;
pub mod subkeys;
use crate::secret_int;
secret_int! {
/// A single DES round subkey (48 bits stored in lower bits of u64).
pub struct Subkey(u64, 48, 0x0000_FFFF_FFFF_FFFF);
}
secret_int! {
/// 56-bit key after PC-1 (lower 56 bits used).
pub struct Key56(u64, 56, 0x00FF_FFFF_FFFF_FFFF);
}
secret_int! {
/// 28-bit half (C or D), stored in lower 28 bits of u32.
pub struct Half28(u32, 28, 0x0FFF_FFFF);
}

84
des/src/key/secret_int.rs Normal file
View File

@ -0,0 +1,84 @@
/// Macro to generate a masked, zeroizable integer wrapper type.
///
/// Usage:
/// ```
/// secret_int! {
/// /// docs...
/// pub struct Subkey(u64, 48, 0x0000_FFFF_FFFF_FFFFu64);
/// }
/// ```
///
/// Optional `clone` token enables an explicit Clone impl:
/// ```
/// secret_int! { pub struct Foo(u32, 28, 0x0FFF_FFFFu32, clone); }
/// ```
#[macro_export]
macro_rules! secret_int {
(
$(#[$meta:meta])*
$vis:vis struct $name:ident ( $int:ty, $bits:expr, $mask:expr $(, $opt:ident )? );
) => {
$(#[$meta])*
#[derive(::zeroize::ZeroizeOnDrop, Default, Eq)]
#[zeroize(drop)]
$vis struct $name($int);
impl $name {
/// Number of meaningful bits.
pub const BITS: usize = $bits;
/// Mask to restrict the underlying integer to valid bits.
pub const MASK: $int = $mask;
/// Create from the given integer.
#[inline]
#[must_use]
pub const fn from_int(key: $int) -> Self {
Self(key & Self::MASK)
}
/// Return as masked integer value;
#[inline]
#[must_use]
pub const fn as_int(&self) -> $int {
self.0 & Self::MASK
}
/// Zero value.
pub const fn zero() -> Self {
Self(0)
}
}
// Optionally add Clone if requested explicitly (discouraged for secrets)
secret_int!(@maybe_add_clone $( $opt )? ; $name, $int);
impl ::std::fmt::Debug for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
f.write_str(concat!(stringify!($name), "[REDACTED]"))
}
}
impl From<$int> for $name {
fn from(v: $int) -> Self {
Self::from_int(v)
}
}
impl PartialEq for $name {
fn eq(&self, other: &Self) -> bool {
self.as_int() == other.as_int()
}
}
};
// helper arm: create Clone impl only when "clone" token present
(@maybe_add_clone clone ; $name:ident, $int:ty) => {
impl Clone for $name {
fn clone(&self) -> Self {
// explicit clone - intentionally duplicating secret
Self::from_int(self.as_int())
}
}
};
(@maybe_add_clone ; $name:ident, $int:ty) => {};
}

154
des/src/key/subkeys.rs Normal file
View File

@ -0,0 +1,154 @@
use crate::key::Subkey;
use cipher_core::{CryptoError, CryptoResult, KeyLike};
use std::fmt::Debug;
/// Key Permutation table (64 to 56 bits).
const PC1: [u8; 56] = [
57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60,
52, 44, 36, 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29,
21, 13, 5, 28, 20, 12, 4,
];
/// Compression Permutation table (56 to 48 bits).
const PC2: [u8; 48] = [
14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2, 41, 52,
31, 37, 47, 55, 30, 40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32,
];
/// Number of Key Bits Shifted per Round
const ROUND_ROTATIONS: [u8; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
/// Container for all 16 round subkeys; zeroized on drop.
pub struct Subkeys([Subkey; 16]);
impl Subkeys {
#[inline]
#[must_use]
pub const fn new_empty() -> Self {
Self([const { Subkey::zero() }; 16])
}
#[inline]
#[must_use]
pub const fn as_array(&self) -> &[Subkey; 16] {
&self.0
}
#[inline]
pub fn set(&mut self, idx: usize, sk: Subkey) {
self.0[idx] = sk;
}
#[inline]
#[must_use]
pub fn get(&self, idx: usize) -> Option<&Subkey> {
self.0.get(idx)
}
pub fn from_key(key: &impl KeyLike) -> CryptoResult<Self> {
let key_bytes = key.as_bytes();
let key_len = key_bytes.len();
if key_len != 8 {
return Err(CryptoError::invalid_key_size(8, key_len));
}
// Produce 56 bits after PC-1 as two 28-bit halves C0 and D0 (MSB-first bit order)
let (c_bits, d_bits) = PC1.iter().enumerate().fold(
([0; 28], [0; 28]),
|(mut c_bits, mut d_bits), (idx, &pos)| {
let bit = get_bit_be(key_bytes, pos);
if idx < 28 {
c_bits[idx] = bit;
} else {
d_bits[idx - 28] = bit;
}
(c_bits, d_bits)
},
);
let mut c = bits_to_u28(&c_bits);
let mut d = bits_to_u28(&d_bits);
let out = ROUND_ROTATIONS.iter().enumerate().fold(
[const { Subkey::zero() }; 16],
|mut acc, (round, &shifts)| {
c = rotate_left_28(c, shifts.into());
d = rotate_left_28(d, shifts.into());
let sub48 = pc2_from_cd(c, d);
acc[round] = Subkey::from(sub48);
acc
},
);
Ok(Self(out))
}
pub(crate) fn as_u64_array(&self) -> [u64; 16] {
self.0
.iter()
.enumerate()
.fold([0; 16], |mut out, (idx, sk)| {
out[idx] = sk.as_int();
out
})
}
}
fn pc2_from_cd(c: u32, d: u32) -> u64 {
let combined: [u8; 56] = (0..28)
.flat_map(|idx| {
let bit_idx = 27 - idx;
[((c >> bit_idx) & 1) as u8, ((d >> bit_idx) & 1) as u8]
})
.collect::<Vec<_>>()
.try_into()
.expect("");
let out = PC2.iter().fold(0, |acc, &pos| {
let bit = u64::from(combined[(pos as usize).saturating_sub(1)]);
(acc << 1) | bit
});
out & 0xFFFF_FFFF_FFFF
}
const U28_MASK: u32 = 0x0FFF_FFFF;
#[inline]
#[must_use]
const fn get_bit_be(bytes: &[u8], pos: u8) -> u8 {
let p = (pos as usize).saturating_sub(1);
let byte_idx = p / 8;
let bit_idx = 7 - (p % 8);
(bytes[byte_idx] >> bit_idx) & 1
}
#[inline]
#[must_use]
fn bits_to_u28(bits: &[u8; 28]) -> u32 {
let mut v = 0;
for &b in bits {
v = (v << 1) | (u32::from(b));
}
v & U28_MASK
}
#[inline]
#[must_use]
const fn rotate_left_28(v: u32, n: u32) -> u32 {
let v = v & U28_MASK;
((v << n) | (v >> (28 - n))) & U28_MASK
}
impl Debug for Subkeys {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Subkeys[REDACTED]")
}
}
impl Default for Subkeys {
fn default() -> Self {
Self::new_empty()
}
}

View File

@ -0,0 +1,5 @@
pub(crate) mod constants;
mod des;
mod key;
pub use des::Des;