Skip to main content

base64_ng_subtle/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![deny(unsafe_code)]
3#![deny(missing_docs)]
4#![deny(clippy::all)]
5#![deny(clippy::pedantic)]
6
7//! Optional `subtle::ConstantTimeEq` integration for `base64-ng`.
8//!
9//! The core `base64-ng` package stays zero-runtime-dependency. This companion
10//! crate exists for applications that already admit `subtle` and want a
11//! reviewed comparison primitive at the protocol boundary.
12//!
13//! Length is treated as public. Mismatched lengths return
14//! [`subtle::Choice::from(0)`] immediately. Use fixed-size protocol tokens when
15//! length must not vary. When the length itself is secret, compare fixed-size
16//! arrays or fixed-width protocol buffers directly with
17//! [`subtle::ConstantTimeEq`] instead of this public-length helper.
18
19use base64_ng::{DecodedBuffer, EncodedBuffer};
20use subtle::{Choice, ConstantTimeEq};
21
22#[cfg(feature = "alloc")]
23use base64_ng::SecretBuffer;
24
25/// Extension trait for comparing `base64-ng` buffers with `subtle`.
26///
27/// The comparison delegates equal-length byte comparisons to
28/// [`subtle::ConstantTimeEq`]. Length mismatch remains public and returns
29/// `Choice::from(0)`.
30pub trait SubtleEqExt {
31    /// Compares `self` with `expected` using `subtle` for equal-length inputs.
32    ///
33    /// Length is public. If lengths differ, this returns `Choice::from(0)`.
34    #[must_use = "use Choice or convert it deliberately with bool::from(choice)"]
35    fn subtle_ct_eq(&self, expected: &[u8]) -> Choice;
36
37    /// Convenience boolean wrapper around [`Self::subtle_ct_eq`].
38    ///
39    /// Prefer [`Self::subtle_ct_eq`] when composing with other `subtle`
40    /// decisions.
41    #[must_use]
42    fn subtle_verify(&self, expected: &[u8]) -> bool {
43        bool::from(self.subtle_ct_eq(expected))
44    }
45}
46
47impl SubtleEqExt for [u8] {
48    fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
49        subtle_ct_eq_public_len(self, expected)
50    }
51}
52
53impl SubtleEqExt for &[u8] {
54    fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
55        subtle_ct_eq_public_len(self, expected)
56    }
57}
58
59impl<const CAP: usize> SubtleEqExt for DecodedBuffer<CAP> {
60    fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
61        subtle_ct_eq_public_len(self.as_bytes(), expected)
62    }
63}
64
65impl<const CAP: usize> SubtleEqExt for EncodedBuffer<CAP> {
66    fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
67        subtle_ct_eq_public_len(self.as_bytes(), expected)
68    }
69}
70
71#[cfg(feature = "alloc")]
72impl SubtleEqExt for SecretBuffer {
73    fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
74        subtle_ct_eq_public_len(self.expose_secret(), expected)
75    }
76}
77
78/// Compares two byte slices with public length.
79///
80/// Equal-length comparisons are delegated to [`subtle::ConstantTimeEq`].
81/// Mismatched lengths return `Choice::from(0)` immediately.
82///
83/// Use [`subtle::ConstantTimeEq`] directly on fixed-size arrays or fixed-width
84/// protocol buffers when token length must not be observable.
85#[must_use = "use Choice or convert it deliberately with bool::from(choice)"]
86pub fn subtle_ct_eq_public_len(left: &[u8], right: &[u8]) -> Choice {
87    if left.len() == right.len() {
88        left.ct_eq(right)
89    } else {
90        Choice::from(0)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::{SubtleEqExt, subtle_ct_eq_public_len};
97    use base64_ng::STANDARD;
98
99    #[cfg(feature = "alloc")]
100    use base64_ng::ct;
101
102    #[test]
103    fn compares_raw_slices_with_public_length() {
104        assert!(bool::from(subtle_ct_eq_public_len(b"hello", b"hello")));
105        assert!(!bool::from(subtle_ct_eq_public_len(b"hello", b"world")));
106        assert!(!bool::from(subtle_ct_eq_public_len(b"hello", b"hello!")));
107    }
108
109    #[test]
110    fn compares_stack_backed_buffers() {
111        let decoded = STANDARD.decode_buffer::<5>(b"aGVsbG8=").unwrap();
112        assert!(decoded.subtle_verify(b"hello"));
113        assert!(!decoded.subtle_verify(b"world"));
114
115        let encoded = STANDARD.encode_buffer::<8>(b"hello").unwrap();
116        assert!(encoded.subtle_verify(b"aGVsbG8="));
117        assert!(!encoded.subtle_verify(b"aGVsbG8h"));
118    }
119
120    #[cfg(feature = "alloc")]
121    #[test]
122    fn compares_secret_buffer() {
123        let decoded = ct::STANDARD.decode_secret(b"aGVsbG8=").unwrap();
124        assert!(decoded.subtle_verify(b"hello"));
125        assert!(!decoded.subtle_verify(b"world"));
126    }
127}