krypteia-arcana 0.1.0

Pure-Rust classical cryptographic primitives: RSA (PKCS#1 v1.5, OAEP), ECC (NIST P-256/384/521, secp256k1), ECDSA, EdDSA (Ed25519), X25519, AES (128/192/256, GCM/CBC), DES/3DES, SHA-1/2/3, HMAC. Side-channel-aware (Montgomery ladder, branchless point_add_ct). Targets embedded (no_std), STM32 M0/M4/M33, ESP32-C3 RISC-V. Zero runtime dependencies.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! RustCrypto trait bridges (feature-gated with `rust-crypto-traits`).
//!
//! When the `rust-crypto-traits` feature is enabled, this module exposes
//! **wrapper types** that implement the standard RustCrypto traits
//! (`digest::Digest`, `digest::ExtendableOutput`, ...) by delegating to
//! our native primitives. Users who want to plug arcana into
//! the wider Rust crypto ecosystem (HMAC, HKDF, PBKDF2, MAC traits,
//! ...) use these wrappers; users who don't enable the feature don't
//! pay the dependency cost.
//!
//! # Why wrappers and not direct impls?
//!
//! We deliberately do **not** implement `digest::Digest` directly on
//! our native `crate::hash::Sha256` etc. Two reasons:
//!
//! 1. **Isolation**: all the RustCrypto glue lives in this one file.
//!    The native modules stay focused on their cryptographic primitive
//!    and are independent of any external ecosystem's version churn.
//!
//! 2. **No accidental ambient trait exposure**: if we impl'd
//!    `Digest` directly, adding a new method to our native `Hasher`
//!    trait could inadvertently clash with the same-named method
//!    from `digest::Digest` on the native type. Wrappers keep the
//!    two trait universes cleanly separated.
//!
//! The performance cost of the wrapper is zero: it is a newtype over
//! the native state and the trait impls are all `#[inline]`
//! single-line delegations.
//!
//! # Example
//!
//! ```rust,ignore
//! use arcana::bridge::Sha256;
//! use digest::Digest;
//!
//! // Generic over any `digest::Digest` implementor, including our
//! // Sha256 wrapper.
//! fn hash<D: Digest>(data: &[u8]) -> Vec<u8> {
//!     let mut h = D::new();
//!     h.update(data);
//!     h.finalize().to_vec()
//! }
//!
//! let out = hash::<Sha256>(b"hello");
//! assert_eq!(out.len(), 32);
//! ```
//!
//! # Scope
//!
//! This commit bridges:
//!
//! | Native type         | Wrapper        | Traits                              |
//! |---------------------|----------------|-------------------------------------|
//! | `hash::Sha1`        | `Sha1`         | `Digest` (20-byte output)           |
//! | `hash::Sha256`      | `Sha256`       | `Digest` (32-byte output)           |
//! | `hash::Sha384`     | `Sha384`       | `Digest` (48-byte output)           |
//! | `hash::Sha512`      | `Sha512`       | `Digest` (64-byte output)           |
//! | `hash::Sha3_256`    | `Sha3_256`     | `Digest` (32-byte output)           |
//! | `hash::Sha3_384`    | `Sha3_384`     | `Digest` (48-byte output)           |
//! | `hash::Sha3_512`    | `Sha3_512`     | `Digest` (64-byte output)           |
//! | `hash::Ripemd160`   | `Ripemd160`    | `Digest` (20-byte output)           |
//! | `hash::Shake128`    | `Shake128`     | `ExtendableOutput`                  |
//! | `hash::Shake256`    | `Shake256`     | `ExtendableOutput`                  |
//!
//! Block-cipher and signature bridges are a follow-up ticket (the
//! `cipher` 0.4 and `signature` 2.0 trait machinery is more involved
//! and deserves its own commit).

#![cfg(feature = "rust-crypto-traits")]

use digest::{
    FixedOutput, HashMarker, Output, OutputSizeUser, Reset, Update, XofReader,
    typenum::{U20, U32, U48, U64},
};

use crate::Hasher as NativeHasher;
use crate::Xof as NativeXof;

// ============================================================================
// Fixed-output hash wrappers (SHA-1, SHA-2, SHA-3, RIPEMD-160)
// ============================================================================

/// Emit a wrapper newtype + the 5 trait impls required for the
/// `digest::Digest` blanket impl to fire:
/// `OutputSizeUser`, `HashMarker`, `Update`, `FixedOutput`, `Default`.
///
/// Also implements `Reset` and `FixedOutputReset` so callers get
/// `Digest::finalize_reset` / `finalize_into_reset` for free.
macro_rules! bridge_fixed_hash {
    ($wrapper:ident, $native:path, $out_size:ty, $doc:literal) => {
        #[doc = $doc]
        #[derive(Clone)]
        pub struct $wrapper($native);

        impl Default for $wrapper {
            #[inline]
            fn default() -> Self {
                Self(<$native as NativeHasher>::new())
            }
        }

        impl OutputSizeUser for $wrapper {
            type OutputSize = $out_size;
        }

        impl HashMarker for $wrapper {}

        impl Update for $wrapper {
            #[inline]
            fn update(&mut self, data: &[u8]) {
                NativeHasher::update(&mut self.0, data);
            }
        }

        impl FixedOutput for $wrapper {
            #[inline]
            fn finalize_into(self, out: &mut Output<Self>) {
                NativeHasher::finalize_into(self.0, out.as_mut_slice());
            }
        }

        impl Reset for $wrapper {
            #[inline]
            fn reset(&mut self) {
                self.0 = <$native as NativeHasher>::new();
            }
        }

        impl digest::FixedOutputReset for $wrapper {
            #[inline]
            fn finalize_into_reset(&mut self, out: &mut Output<Self>) {
                let taken = core::mem::replace(&mut self.0, <$native as NativeHasher>::new());
                NativeHasher::finalize_into(taken, out.as_mut_slice());
            }
        }
    };
}

bridge_fixed_hash!(
    Sha1,
    crate::hash::sha1::Sha1,
    U20,
    "`digest::Digest` wrapper for [`crate::hash::sha1::Sha1`] (SHA-1, 160-bit)."
);
bridge_fixed_hash!(
    Sha256,
    crate::hash::sha256::Sha256,
    U32,
    "`digest::Digest` wrapper for [`crate::hash::sha256::Sha256`]."
);
bridge_fixed_hash!(
    Sha384,
    crate::hash::sha384::Sha384,
    U48,
    "`digest::Digest` wrapper for [`crate::hash::sha384::Sha384`]."
);
bridge_fixed_hash!(
    Sha512,
    crate::hash::sha512::Sha512,
    U64,
    "`digest::Digest` wrapper for [`crate::hash::sha512::Sha512`]."
);
bridge_fixed_hash!(
    Sha3_256,
    crate::hash::sha3::Sha3_256,
    U32,
    "`digest::Digest` wrapper for [`crate::hash::sha3::Sha3_256`]."
);
bridge_fixed_hash!(
    Sha3_384,
    crate::hash::sha3::Sha3_384,
    U48,
    "`digest::Digest` wrapper for [`crate::hash::sha3::Sha3_384`]."
);
bridge_fixed_hash!(
    Sha3_512,
    crate::hash::sha3::Sha3_512,
    U64,
    "`digest::Digest` wrapper for [`crate::hash::sha3::Sha3_512`]."
);
bridge_fixed_hash!(
    Ripemd160,
    crate::hash::ripemd160::Ripemd160,
    U20,
    "`digest::Digest` wrapper for [`crate::hash::ripemd160::Ripemd160`] (RIPEMD-160, 160-bit)."
);

// ============================================================================
// Extendable-output (XOF) wrappers: SHAKE128 / SHAKE256
// ============================================================================

/// Common `XofReader` wrapper: holds an already-initialised native
/// XOF and yields bytes on demand by calling its `squeeze`.
///
/// Used for both SHAKE128 and SHAKE256 since they share the same
/// native `Xof` interface and only differ in construction.
pub struct XofStream<X: NativeXof>(X);

impl<X: NativeXof> XofReader for XofStream<X> {
    #[inline]
    fn read(&mut self, buffer: &mut [u8]) {
        self.0.squeeze(buffer);
    }
}

macro_rules! bridge_xof {
    ($wrapper:ident, $native:path, $doc:literal) => {
        #[doc = $doc]
        #[derive(Clone, Default)]
        pub struct $wrapper($native);

        impl Update for $wrapper {
            #[inline]
            fn update(&mut self, data: &[u8]) {
                NativeXof::update(&mut self.0, data);
            }
        }

        impl Reset for $wrapper {
            #[inline]
            fn reset(&mut self) {
                self.0 = <$native as NativeXof>::new();
            }
        }

        impl digest::ExtendableOutput for $wrapper {
            type Reader = XofStream<$native>;

            #[inline]
            fn finalize_xof(self) -> Self::Reader {
                XofStream(self.0)
            }
        }
    };
}

// Shake128 / Shake256 don't have Default in the native trait, and
// `Default::default()` on the wrapper uses the inner field's default
// via #[derive(Default)]. We add a manual Default on the native
// types indirectly by relying on Default derive working over the
// inner Shake128/Shake256 type -- but those don't derive Default
// either. So we override Default manually:
impl Default for crate::hash::sha3::Shake128 {
    fn default() -> Self {
        <Self as NativeXof>::new()
    }
}
impl Default for crate::hash::sha3::Shake256 {
    fn default() -> Self {
        <Self as NativeXof>::new()
    }
}

bridge_xof!(
    Shake128,
    crate::hash::sha3::Shake128,
    "`digest::ExtendableOutput` wrapper for [`crate::hash::sha3::Shake128`]."
);
bridge_xof!(
    Shake256,
    crate::hash::sha3::Shake256,
    "`digest::ExtendableOutput` wrapper for [`crate::hash::sha3::Shake256`]."
);

// ============================================================================
// Tests — use the wrappers through the RustCrypto `Digest` trait and
// verify they produce byte-identical output to our native hashers.
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use digest::{Digest as RcDigest, ExtendableOutput as RcExtendableOutput};

    /// SHA-256 empty hash via the `digest::Digest` blanket impl.
    /// Pinned against FIPS 180-4 Appendix A.
    #[test]
    fn bridge_sha256_empty_matches_fips() {
        let out = <Sha256 as RcDigest>::digest(b"");
        let expected = hex_lit::hex("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
        assert_eq!(out.as_slice(), expected.as_slice());
    }

    /// SHA-256 "abc" pinned vector, via the trait.
    #[test]
    fn bridge_sha256_abc_matches_fips() {
        let out = <Sha256 as RcDigest>::digest(b"abc");
        let expected = hex_lit::hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
        assert_eq!(out.as_slice(), expected.as_slice());
    }

    /// Cross-check: the trait path and our native `Hasher::hash` must
    /// produce the same bytes for every fixed-output wrapper.
    ///
    /// Note the fully-qualified native paths here: `crate::hash::*`
    /// re-exports `Sha1`, `Sha256`, etc. with the same names as our
    /// bridge wrappers, so a glob import would shadow them.
    #[test]
    fn bridge_all_fixed_hashes_match_native() {
        let data = b"cross-check";

        assert_eq!(
            <Sha1 as RcDigest>::digest(data).as_slice(),
            <crate::hash::sha1::Sha1 as NativeHasher>::hash(data).as_slice(),
        );
        assert_eq!(
            <Sha256 as RcDigest>::digest(data).as_slice(),
            <crate::hash::sha256::Sha256 as NativeHasher>::hash(data).as_slice(),
        );
        assert_eq!(
            <Sha384 as RcDigest>::digest(data).as_slice(),
            <crate::hash::sha384::Sha384 as NativeHasher>::hash(data).as_slice(),
        );
        assert_eq!(
            <Sha512 as RcDigest>::digest(data).as_slice(),
            <crate::hash::sha512::Sha512 as NativeHasher>::hash(data).as_slice(),
        );
        assert_eq!(
            <Sha3_256 as RcDigest>::digest(data).as_slice(),
            <crate::hash::sha3::Sha3_256 as NativeHasher>::hash(data).as_slice(),
        );
        assert_eq!(
            <Sha3_384 as RcDigest>::digest(data).as_slice(),
            <crate::hash::sha3::Sha3_384 as NativeHasher>::hash(data).as_slice(),
        );
        assert_eq!(
            <Sha3_512 as RcDigest>::digest(data).as_slice(),
            <crate::hash::sha3::Sha3_512 as NativeHasher>::hash(data).as_slice(),
        );
        assert_eq!(
            <Ripemd160 as RcDigest>::digest(data).as_slice(),
            <crate::hash::ripemd160::Ripemd160 as NativeHasher>::hash(data).as_slice(),
        );
    }

    /// Incremental `update` via the trait must match the one-shot path.
    /// Uses fully-qualified call syntax because both `digest::Digest`
    /// and `digest::Update` are in scope and both provide `update`.
    #[test]
    fn bridge_incremental_update_matches_oneshot() {
        let mut h = <Sha256 as RcDigest>::new();
        RcDigest::update(&mut h, b"abc");
        RcDigest::update(&mut h, b"def");
        let incremental = h.finalize();
        let oneshot = <Sha256 as RcDigest>::digest(b"abcdef");
        assert_eq!(incremental, oneshot);
    }

    /// `finalize_reset` must reset state so the next hash starts clean.
    #[test]
    fn bridge_finalize_reset_leaves_clean_state() {
        let mut h = <Sha256 as RcDigest>::new();
        RcDigest::update(&mut h, b"first");
        let first = h.finalize_reset();
        RcDigest::update(&mut h, b"second");
        let second = h.finalize();
        assert_ne!(first, second);
        assert_eq!(first, <Sha256 as RcDigest>::digest(b"first"));
        assert_eq!(second, <Sha256 as RcDigest>::digest(b"second"));
    }

    /// SHAKE128 through the `ExtendableOutput` trait must match the
    /// native `squeeze` path byte-for-byte.
    #[test]
    fn bridge_shake128_matches_native() {
        let data = b"shake it up";

        // Through the bridge.
        let mut bridge = Shake128::default();
        Update::update(&mut bridge, data);
        let mut reader = bridge.finalize_xof();
        let mut out_bridge = [0u8; 64];
        reader.read(&mut out_bridge);

        // Through the native API.
        let mut native = <crate::hash::sha3::Shake128 as NativeXof>::new();
        NativeXof::update(&mut native, data);
        let mut out_native = [0u8; 64];
        NativeXof::squeeze(&mut native, &mut out_native);

        assert_eq!(out_bridge, out_native);
    }

    /// SHAKE128 reader can be read in multiple chunks and must yield
    /// the same sequence as a single big read (because the XOF state
    /// is stateful across squeeze calls).
    #[test]
    fn bridge_shake128_chunked_read_matches_single() {
        let data = b"hello shake";

        let mut bridge1 = Shake128::default();
        Update::update(&mut bridge1, data);
        let mut reader1 = bridge1.finalize_xof();
        let mut full = [0u8; 80];
        reader1.read(&mut full);

        let mut bridge2 = Shake128::default();
        Update::update(&mut bridge2, data);
        let mut reader2 = bridge2.finalize_xof();
        let mut chunked = [0u8; 80];
        reader2.read(&mut chunked[..32]);
        reader2.read(&mut chunked[32..48]);
        reader2.read(&mut chunked[48..]);

        assert_eq!(full, chunked);
    }

    /// A tiny local hex literal helper so this test module does not
    /// need a proc-macro crate dependency.
    mod hex_lit {
        pub fn hex(s: &str) -> Vec<u8> {
            assert!(s.len() % 2 == 0);
            (0..s.len())
                .step_by(2)
                .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
                .collect()
        }
    }
}