///|
/// DES and TripleDES (FIPS 46-3), self-implemented for OpenPGP symmetric
/// algorithm 2 (TripleDES) — a read-side requirement: RFC 4880 §9.2 makes
/// 3DES a MUST-implement and old GnuPG/PGP output uses it. OpenPGP never
/// *generates* 3DES in this library; the dispatch in `blockcipher.mbt`
/// only routes legacy payloads here. Keying: three independent 8-octet
/// DES keys (24 octets, EDE order k1|k2|k3).
///
/// The algorithm structure follows the reference formulation: block
/// permutations, the 16-round Feistel with expansion/E-switch/P, the
/// PC1/PC2 key schedule with its rotation calendar, and the S-boxes. The
/// fused Feistel box (S-box output pre-permuted through P, the data
/// rotation factored in) is the standard tabulation of that formulation.
///|
/// General block permutation: bit `perm[position]` of `src` (0-based,
/// most-significant-first) becomes bit `width - 1 - position` of the
/// result.
fn des_permute(src : UInt64, perm : Array[Int32], width : Int32) -> UInt64 {
let mut block = 0UL
for position in 0..> perm[position]) & 1UL
block = block | (bit << (width - 1 - position))
}
block
}
///|
fn des_be_u64(view : BytesView) -> UInt64 {
let mut v = 0UL
for i in 0..<8 {
v = (v << 8) | view[i].to_int().to_uint64()
}
v
}
///|
fn des_u64_to_be(v : UInt64) -> Bytes {
v.to_be_bytes()
}
///|
/// 48-bit PC2 output repacked so each 6-bit key chunk sits in one byte
/// (two high bits zero) — this is what lets the Feistel rounds XOR whole
/// 32-bit halves of the subkey against the expanded data.
fn des_unpack(x : UInt64) -> UInt64 {
(((x >> (6 * 1)) & 0xffUL) << (8 * 0)) |
(((x >> (6 * 3)) & 0xffUL) << (8 * 1)) |
(((x >> (6 * 5)) & 0xffUL) << (8 * 2)) |
(((x >> (6 * 7)) & 0xffUL) << (8 * 3)) |
(((x >> (6 * 0)) & 0xffUL) << (8 * 4)) |
(((x >> (6 * 2)) & 0xffUL) << (8 * 5)) |
(((x >> (6 * 4)) & 0xffUL) << (8 * 6)) |
(((x >> (6 * 6)) & 0xffUL) << (8 * 7))
}
///|
/// The fused Feistel box: `box[s][t]` is the P-permuted S-box output for
/// S-box `s`, 6-bit input `t`, with the 1-bit data rotation of the round
/// function already folded in (so the round XORs boxes against the
/// *unrotated* half). 32-bit values carried in UInt64.
fn des_feistel_box() -> Array[Array[UInt64]] {
Array::makei(8, s => {
let inner = FixedArray::make(64, 0UL)
for i in 0..<4 {
for j in 0..<16 {
let mut f = des_sboxes[s][i * 16 + j].to_uint64() << (4 * (7 - s))
f = des_permute(f, des_p, 32)
// Row is the 1st and 6th S-box address bit, column the middle
// four — expressed over the 8-bit-chunk layout of `des_unpack`.
let row = (((i & 2) << 4) | (i & 1)).to_uint64()
let col = (j << 1).to_uint64()
let t = (row | col).to_int()
// The rotation was performed in the rounds; here it is folded in.
f = (f << 1) | (f >> 31)
inner[t] = f & 0xFFFFFFFFUL
}
}
Array::from_iter(inner.iter())
})
}
///|
/// One key half through the 16-step 28-bit rotation calendar.
fn des_ks_rotate(half0 : UInt64) -> Array[UInt64] {
let mut half = half0
let out : Array[UInt64] = []
for i in 0..<16 {
let r = des_rotations[i]
// 28-bit circular left shift (mask because UInt64 does not truncate
// the way the reference's uint32 arithmetic does).
let v = ((half << r) | (half >> (28 - r))) & 0x0FFFFFFFUL
out.push(v)
half = v
}
out
}
///|
/// Expand one 8-octet DES key into its 16 unpacked round subkeys.
fn des_generate_subkeys(key : BytesView) -> Array[UInt64] raise {
guard key.length() == 8 else { @error.fail("des: key must be 8 octets") }
let permuted = des_permute(des_be_u64(key), des_pc1, 56)
let left = des_ks_rotate((permuted >> 28) & 0x0FFFFFFFUL)
let right = des_ks_rotate(permuted & 0x0FFFFFFFUL)
Array::makei(16, i => {
let pc2_input = (left[i] << 28) | right[i]
des_unpack(des_permute(pc2_input, des_pc2, 48))
})
}
///|
/// The 32-bit Feistel function over two half-rounds, per the fused box.
/// All values are 32-bit quantities in UInt64; XOR and shifts need no
/// masks, so only the box outputs are pre-truncated.
fn des_feistel(
box : Array[Array[UInt64]],
l : UInt64,
r : UInt64,
k0 : UInt64,
k1 : UInt64,
) -> (UInt64, UInt64) {
let mut l = l
let mut r = r
let mut t = r ^ (k0 >> 32)
l = l ^
(
box[7][t.to_int() & 0x3f] ^
box[5][(t >> 8).to_int() & 0x3f] ^
box[3][(t >> 16).to_int() & 0x3f] ^
box[1][(t >> 24).to_int() & 0x3f]
)
t = (((r << 28) | (r >> 4)) & 0xFFFFFFFFUL) ^ (k0 & 0xFFFFFFFFUL)
l = l ^
(
box[6][t.to_int() & 0x3f] ^
box[4][(t >> 8).to_int() & 0x3f] ^
box[2][(t >> 16).to_int() & 0x3f] ^
box[0][(t >> 24).to_int() & 0x3f]
)
t = l ^ (k1 >> 32)
r = r ^
(
box[7][t.to_int() & 0x3f] ^
box[5][(t >> 8).to_int() & 0x3f] ^
box[3][(t >> 16).to_int() & 0x3f] ^
box[1][(t >> 24).to_int() & 0x3f]
)
t = (((l << 28) | (l >> 4)) & 0xFFFFFFFFUL) ^ (k1 & 0xFFFFFFFFUL)
r = r ^
(
box[6][t.to_int() & 0x3f] ^
box[4][(t >> 8).to_int() & 0x3f] ^
box[2][(t >> 16).to_int() & 0x3f] ^
box[0][(t >> 24).to_int() & 0x3f]
)
(l, r)
}
///|
/// DES (and TripleDES) state: expanded subkey sets plus the shared
/// Feistel box, built once per cipher instance.
priv struct DesState {
box : Array[Array[UInt64]]
/// One subkey array per DES key (1 for DES, 3 for 3DES), EDE order.
keys : Array[Array[UInt64]]
}
///|
fn DesState::crypt(self : DesState, src : BytesView, decrypt : Bool) -> Bytes {
if src.length() != 8 {
abort("des: block must be 8 octets")
}
let b = des_permute(des_be_u64(src), des_ip, 64)
let mut left = (b >> 32) & 0xFFFFFFFFUL
let mut right = b & 0xFFFFFFFFUL
// The rounds operate on 31-rotated halves; undo before the final
// permutation (the rotation is folded into the box instead).
left = ((left << 1) | (left >> 31)) & 0xFFFFFFFFUL
right = ((right << 1) | (right >> 31)) & 0xFFFFFFFFUL
if decrypt {
for i in 0..<8 {
let sub = self.keys[self.keys.length() - 1]
let (l, r) = des_feistel(
self.box,
left,
right,
sub[15 - 2 * i],
sub[15 - (2 * i + 1)],
)
left = l
right = r
}
// Middle key with the half-swap convention, then the first key.
if self.keys.length() == 3 {
let mid = self.keys[1]
for i in 0..<8 {
let (rl, rr) = des_feistel(
self.box,
right,
left,
mid[2 * i],
mid[2 * i + 1],
)
left = rr
right = rl
}
let first = self.keys[0]
for i in 0..<8 {
let (l, r) = des_feistel(
self.box,
left,
right,
first[15 - 2 * i],
first[15 - (2 * i + 1)],
)
left = l
right = r
}
}
} else {
for i in 0..<8 {
let sub = self.keys[0]
let (l, r) = des_feistel(
self.box,
left,
right,
sub[2 * i],
sub[2 * i + 1],
)
left = l
right = r
}
if self.keys.length() == 3 {
let mid = self.keys[1]
for i in 0..<8 {
let (rl, rr) = des_feistel(
self.box,
right,
left,
mid[15 - 2 * i],
mid[15 - (2 * i + 1)],
)
left = rr
right = rl
}
let last = self.keys[2]
for i in 0..<8 {
let (l, r) = des_feistel(
self.box,
left,
right,
last[2 * i],
last[2 * i + 1],
)
left = l
right = r
}
}
}
left = ((left << 31) | (left >> 1)) & 0xFFFFFFFFUL
right = ((right << 31) | (right >> 1)) & 0xFFFFFFFFUL
let pre_output = (right << 32) | left
des_u64_to_be(des_permute(pre_output, des_fp, 64))
}
///|
impl BlockCipher for DesState with fn block_size(_self) -> Int32 {
8
}
///|
impl BlockCipher for DesState with fn encrypt_block(self, block) -> Bytes {
self.crypt(block, false)
}
///|
impl BlockCipher for DesState with fn decrypt_block(self, block) -> Bytes {
self.crypt(block, true)
}
///|
impl BlockCipher for DesState with fn wipe(self) -> Unit {
for keys in self.keys {
for i in 0.. &BlockCipher raise {
guard key.length() == 24 else {
@error.fail("3DES init failed: key must be 24 octets")
}
DesState::{
box: des_feistel_box(),
keys: [
des_generate_subkeys(key[0:8]),
des_generate_subkeys(key[8:16]),
des_generate_subkeys(key[16:24]),
],
}
}
///|
/// Single DES — exposed for the FIPS known-answer tests only.
pub fn new_des(key : BytesView) -> &BlockCipher raise {
guard key.length() == 8 else {
@error.fail("DES init failed: key must be 8 octets")
}
DesState::{ box: des_feistel_box(), keys: [des_generate_subkeys(key)], }
}