f4jumble/
lib.rs

1//! This crate provides a mechanism for "jumbling" byte slices in a reversible way.
2//!
3//! Many byte encodings such as [Base64] and [Bech32] do not have "cascading" behaviour:
4//! changing an input byte at one position has no effect on the encoding of bytes at
5//! distant positions. This can be a problem if users generally check the correctness of
6//! encoded strings by eye, as they will tend to only check the first and/or last few
7//! characters of the encoded string. In some situations (for example, a hardware device
8//! displaying on its screen an encoded string provided by an untrusted computer), it is
9//! potentially feasible for an adversary to change some internal portion of the encoded
10//! string in a way that is beneficial to them, without the user noticing.
11//!
12//! [Base64]: https://en.wikipedia.org/wiki/Base64
13//! [Bech32]: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32
14//!
15//! The function F4Jumble (and its inverse function, F4Jumble⁻¹) are length-preserving
16//! transformations can be used to trivially introduce cascading behaviour to existing
17//! encodings:
18//! - Prepare the raw `message` bytes as usual.
19//! - Pass `message` through [`f4jumble`] or [`f4jumble_mut`] to obtain the jumbled bytes.
20//! - Encode the jumbled bytes with the encoding scheme.
21//!
22//! Changing any byte of `message` will result in a completely different sequence of
23//! jumbled bytes. Specifically, F4Jumble uses an unkeyed 4-round Feistel construction to
24//! approximate a random permutation.
25//!
26//! ![Diagram of 4-round unkeyed Feistel construction](https://zips.z.cash/zip-0316-f4.png)
27//!
28//! ## Efficiency
29//!
30//! The cost is dominated by 4 BLAKE2b compressions for message lengths up to 128 bytes.
31//! For longer messages, the cost increases to 6 BLAKE2b compressions for 128 < lₘ ≤ 192,
32//! and 10 BLAKE2b compressions for 192 < lₘ ≤ 256, for example. The maximum cost for
33//! which the algorithm is defined would be 196608 BLAKE2b compressions at lₘ = 4194368.
34//!
35//! The implementations in this crate require memory of roughly lₘ bytes plus the size of
36//! a BLAKE2b hash state. It is possible to reduce this by (for example, with F4Jumble⁻¹)
37//! streaming the `d` part of the jumbled encoding three times from a less
38//! memory-constrained device. It is essential that the streamed value of `d` is the same
39//! on each pass, which can be verified using a Message Authentication Code (with key held
40//! only by the Consumer) or collision-resistant hash function. After the first pass of
41//! `d`, the implementation is able to compute `y`; after the second pass it is able to
42//! compute `a`; and the third allows it to compute and incrementally parse `b`. The
43//! maximum memory usage during this process would be 128 bytes plus two BLAKE2b hash
44//! states.
45
46#![no_std]
47#![cfg_attr(docsrs, feature(doc_cfg))]
48
49use blake2b_simd::{Params as Blake2bParams, OUTBYTES};
50
51use core::cmp::min;
52use core::fmt;
53use core::ops::RangeInclusive;
54use core::result::Result;
55
56#[cfg(feature = "alloc")]
57extern crate alloc;
58
59#[cfg(feature = "std")]
60#[macro_use]
61extern crate std;
62
63#[cfg(feature = "alloc")]
64use alloc::vec::Vec;
65
66#[cfg(test)]
67mod test_vectors;
68#[cfg(all(test, feature = "std"))]
69mod test_vectors_long;
70
71/// Length of F4Jumbled message must lie in the range VALID_LENGTH.
72///
73/// VALID_LENGTH = 48..=4194368
74pub const VALID_LENGTH: RangeInclusive<usize> = 48..=4194368;
75
76/// Errors produced by F4Jumble.
77#[derive(Debug)]
78pub enum Error {
79    /// Value error indicating that length of F4Jumbled message does not
80    /// lie in the range [`VALID_LENGTH`].
81    InvalidLength,
82}
83
84impl fmt::Display for Error {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Self::InvalidLength => write!(
88                f,
89                "Message length must be in interval ({}..={})",
90                *VALID_LENGTH.start(),
91                *VALID_LENGTH.end()
92            ),
93        }
94    }
95}
96
97#[cfg(feature = "std")]
98impl std::error::Error for Error {}
99
100macro_rules! H_PERS {
101    ( $i:expr ) => {
102        [
103            85, 65, 95, 70, 52, 74, 117, 109, 98, 108, 101, 95, 72, $i, 0, 0,
104        ]
105    };
106}
107
108macro_rules! G_PERS {
109    ( $i:expr, $j:expr ) => {
110        [
111            85,
112            65,
113            95,
114            70,
115            52,
116            74,
117            117,
118            109,
119            98,
120            108,
121            101,
122            95,
123            71,
124            $i,
125            ($j & 0xFF) as u8,
126            ($j >> 8) as u8,
127        ]
128    };
129}
130
131struct State<'a> {
132    left: &'a mut [u8],
133    right: &'a mut [u8],
134}
135
136impl<'a> State<'a> {
137    fn new(message: &'a mut [u8]) -> Self {
138        let left_length = min(OUTBYTES, message.len() / 2);
139        let (left, right) = message.split_at_mut(left_length);
140        State { left, right }
141    }
142
143    fn h_round(&mut self, i: u8) {
144        let hash = Blake2bParams::new()
145            .hash_length(self.left.len())
146            .personal(&H_PERS!(i))
147            .hash(self.right);
148        xor(self.left, hash.as_bytes())
149    }
150
151    fn g_round(&mut self, i: u8) {
152        for j in 0..ceildiv(self.right.len(), OUTBYTES) {
153            let hash = Blake2bParams::new()
154                .hash_length(OUTBYTES)
155                .personal(&G_PERS!(i, j as u16))
156                .hash(self.left);
157            xor(&mut self.right[j * OUTBYTES..], hash.as_bytes());
158        }
159    }
160
161    fn apply_f4jumble(&mut self) {
162        self.g_round(0);
163        self.h_round(0);
164        self.g_round(1);
165        self.h_round(1);
166    }
167
168    fn apply_f4jumble_inv(&mut self) {
169        self.h_round(1);
170        self.g_round(1);
171        self.h_round(0);
172        self.g_round(0);
173    }
174}
175
176/// XORs bytes of the `source` to bytes of the `target`.
177///
178/// This method operates over the first `min(source.len(), target.len())` bytes.
179fn xor(target: &mut [u8], source: &[u8]) {
180    for (source, target) in source.iter().zip(target.iter_mut()) {
181        *target ^= source;
182    }
183}
184
185fn ceildiv(num: usize, den: usize) -> usize {
186    (num + den - 1) / den
187}
188
189/// Encodes the given message in-place using F4Jumble.
190///
191/// Returns an error if the message is an invalid length. `message` will be unmodified in
192/// this case.
193///
194/// # Examples
195///
196/// ```
197/// let mut message_a = *b"The package from Alice arrives tomorrow morning.";
198/// f4jumble::f4jumble_mut(&mut message_a[..]).unwrap();
199/// assert_eq!(
200///     hex::encode(message_a),
201///     "861c51ee746b0313476967a3483e7e1ff77a2952a17d3ed9e0ab0f502e1179430322da9967b613545b1c36353046ca27",
202/// );
203///
204/// let mut message_b = *b"The package from Sarah arrives tomorrow morning.";
205/// f4jumble::f4jumble_mut(&mut message_b[..]).unwrap();
206/// assert_eq!(
207///     hex::encode(message_b),
208///     "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8",
209/// );
210/// ```
211pub fn f4jumble_mut(message: &mut [u8]) -> Result<(), Error> {
212    if VALID_LENGTH.contains(&message.len()) {
213        State::new(message).apply_f4jumble();
214        Ok(())
215    } else {
216        Err(Error::InvalidLength)
217    }
218}
219
220/// Decodes the given message in-place using F4Jumble⁻¹.
221///
222/// Returns an error if the message is an invalid length. `message` will be unmodified in
223/// this case.
224///
225/// # Examples
226///
227/// ```
228/// let mut message_a = hex::decode(
229///     "861c51ee746b0313476967a3483e7e1ff77a2952a17d3ed9e0ab0f502e1179430322da9967b613545b1c36353046ca27")
230///     .unwrap();
231/// f4jumble::f4jumble_inv_mut(&mut message_a).unwrap();
232/// assert_eq!(message_a, b"The package from Alice arrives tomorrow morning.");
233///
234/// let mut message_b = hex::decode(
235///     "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8")
236///     .unwrap();
237/// f4jumble::f4jumble_inv_mut(&mut message_b).unwrap();
238/// assert_eq!(message_b, b"The package from Sarah arrives tomorrow morning.");
239/// ```
240pub fn f4jumble_inv_mut(message: &mut [u8]) -> Result<(), Error> {
241    if VALID_LENGTH.contains(&message.len()) {
242        State::new(message).apply_f4jumble_inv();
243        Ok(())
244    } else {
245        Err(Error::InvalidLength)
246    }
247}
248
249/// Encodes the given message using F4Jumble, and returns the encoded message as a vector
250/// of bytes.
251///
252/// Returns an error if the message is an invalid length.
253///
254/// # Examples
255///
256/// ```
257/// let message_a = b"The package from Alice arrives tomorrow morning.";
258/// let encoded_a = f4jumble::f4jumble(message_a).unwrap();
259/// assert_eq!(
260///     hex::encode(encoded_a),
261///     "861c51ee746b0313476967a3483e7e1ff77a2952a17d3ed9e0ab0f502e1179430322da9967b613545b1c36353046ca27",
262/// );
263///
264/// let message_b = b"The package from Sarah arrives tomorrow morning.";
265/// let encoded_b = f4jumble::f4jumble(message_b).unwrap();
266/// assert_eq!(
267///     hex::encode(encoded_b),
268///     "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8",
269/// );
270/// ```
271#[cfg(feature = "alloc")]
272#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
273pub fn f4jumble(message: &[u8]) -> Result<Vec<u8>, Error> {
274    let mut result = message.to_vec();
275    f4jumble_mut(&mut result).map(|()| result)
276}
277
278/// Decodes the given message using F4Jumble⁻¹, and returns the decoded message as a
279/// vector of bytes.
280///
281/// Returns an error if the message is an invalid length.
282///
283/// # Examples
284///
285/// ```
286/// let encoded_a = hex::decode(
287///     "861c51ee746b0313476967a3483e7e1ff77a2952a17d3ed9e0ab0f502e1179430322da9967b613545b1c36353046ca27")
288///     .unwrap();
289/// let message_a = f4jumble::f4jumble_inv(&encoded_a).unwrap();
290/// assert_eq!(message_a, b"The package from Alice arrives tomorrow morning.");
291///
292/// let encoded_b = hex::decode(
293///     "af1d55f2695aea02440867bbbfae3b08e8da55b625de3fa91432ab7b2c0a7dff9033ee666db1513ba5761ef482919fb8")
294///     .unwrap();
295/// let message_b = f4jumble::f4jumble_inv(&encoded_b).unwrap();
296/// assert_eq!(message_b, b"The package from Sarah arrives tomorrow morning.");
297/// ```
298#[cfg(feature = "alloc")]
299#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
300pub fn f4jumble_inv(message: &[u8]) -> Result<Vec<u8>, Error> {
301    let mut result = message.to_vec();
302    f4jumble_inv_mut(&mut result).map(|()| result)
303}
304
305#[cfg(test)]
306mod common_tests {
307    use super::{f4jumble_inv_mut, f4jumble_mut, test_vectors};
308
309    #[test]
310    fn h_pers() {
311        assert_eq!(&H_PERS!(7), b"UA_F4Jumble_H\x07\x00\x00");
312    }
313
314    #[test]
315    fn g_pers() {
316        assert_eq!(&G_PERS!(7, 13), b"UA_F4Jumble_G\x07\x0d\x00");
317        assert_eq!(&G_PERS!(7, 65535), b"UA_F4Jumble_G\x07\xff\xff");
318    }
319
320    #[test]
321    fn f4jumble_check_vectors_mut() {
322        #[cfg(not(feature = "std"))]
323        let mut cache = [0u8; test_vectors::MAX_VECTOR_LENGTH];
324        #[cfg(feature = "std")]
325        let mut cache = vec![0u8; test_vectors::MAX_VECTOR_LENGTH];
326        for v in test_vectors::TEST_VECTORS {
327            let data = &mut cache[..v.normal.len()];
328            data.clone_from_slice(v.normal);
329            f4jumble_mut(data).unwrap();
330            assert_eq!(data, v.jumbled);
331            f4jumble_inv_mut(data).unwrap();
332            assert_eq!(data, v.normal);
333        }
334    }
335}
336
337#[cfg(feature = "std")]
338#[cfg(test)]
339mod std_tests {
340    use blake2b_simd::blake2b;
341    use proptest::collection::vec;
342    use proptest::prelude::*;
343    use std::format;
344    use std::vec::Vec;
345
346    use super::{f4jumble, f4jumble_inv, test_vectors, test_vectors_long, VALID_LENGTH};
347
348    proptest! {
349        #![proptest_config(ProptestConfig::with_cases(5))]
350
351        #[test]
352        fn f4jumble_roundtrip(msg in vec(any::<u8>(), VALID_LENGTH)) {
353            let jumbled = f4jumble(&msg).unwrap();
354            let jumbled_len = jumbled.len();
355            prop_assert_eq!(
356                msg.len(), jumbled_len,
357                "Jumbled length {} was not equal to message length {}",
358                jumbled_len, msg.len()
359            );
360
361            let unjumbled = f4jumble_inv(&jumbled).unwrap();
362            prop_assert_eq!(
363                jumbled_len, unjumbled.len(),
364                "Unjumbled length {} was not equal to jumbled length {}",
365                unjumbled.len(), jumbled_len
366            );
367
368            prop_assert_eq!(msg, unjumbled, "Unjumbled message did not match original message.");
369        }
370    }
371
372    #[test]
373    fn f4jumble_check_vectors() {
374        for v in test_vectors::TEST_VECTORS {
375            let jumbled = f4jumble(v.normal).unwrap();
376            assert_eq!(jumbled, v.jumbled);
377            let unjumbled = f4jumble_inv(v.jumbled).unwrap();
378            assert_eq!(unjumbled, v.normal);
379        }
380    }
381
382    #[test]
383    fn f4jumble_check_vectors_long() {
384        for v in test_vectors_long::TEST_VECTORS {
385            let normal: Vec<u8> = (0..v.length).map(|i| i as u8).collect();
386            let jumbled = f4jumble(&normal).unwrap();
387            assert_eq!(blake2b(&jumbled).as_bytes(), v.jumbled_hash);
388            let unjumbled = f4jumble_inv(&jumbled).unwrap();
389            assert_eq!(unjumbled, normal);
390        }
391    }
392}