decaf377_fmd/
detection.rs

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
use crate::{hash, hkd, Clue, ClueKey, Error, MAX_PRECISION};
use bitvec::{order, slice::BitSlice};
use decaf377::Fr;
use rand_core::{CryptoRng, RngCore};
use std::convert::{TryFrom, TryInto};

/// Used to examine [`Clue`]s and determine whether they were possibly sent to
/// the detection key's [`ClueKey`].
pub struct DetectionKey {
    /// The detection key.
    dtk: Fr,
    /// Cached copies of the child detection keys; these can be fully derived from `dtk`.
    xs: [Fr; MAX_PRECISION as usize],
}

impl DetectionKey {
    /// Create a random detection key using the supplied `rng`.
    pub fn new<R: RngCore + CryptoRng>(mut rng: R) -> Self {
        Self::from_field(decaf377::Fr::rand(&mut rng))
    }

    /// Create a detection key using the supplied field element directly.
    ///
    /// # Warning
    ///
    /// This function exists to support custom key derivation mechanisms; it's
    /// the caller's responsibility to construct the detection key `dtk`
    /// correctly.
    pub fn from_field(dtk: Fr) -> Self {
        let root_pub = dtk * decaf377::Element::GENERATOR;
        let root_pub_enc = root_pub.vartime_compress();

        let xs: [_; MAX_PRECISION as usize] = (0..MAX_PRECISION as usize)
            .map(|i| {
                hkd::derive_private(
                    &dtk,
                    &root_pub_enc,
                    u8::try_from(i).expect("i < MAX_PRECISION < 256"),
                )
            })
            .collect::<Vec<_>>()
            // this conversion into an array will always succeed because we started with an iterator
            // of length `MAX_PRECISION` and we're converting to an array of the same length
            .try_into()
            .expect("iterator of length `MAX_PRECISION`");

        Self { dtk, xs }
    }

    /// Serialize this detection key to bytes.
    pub fn to_bytes(&self) -> [u8; 32] {
        self.dtk.to_bytes()
    }

    /// Deserialize a detection key from bytes.
    pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, Error> {
        let dtk = Fr::from_bytes_checked(&bytes).map_err(|_| Error::InvalidDetectionKey)?;
        Ok(Self::from_field(dtk))
    }

    /// Obtain the clue key corresponding to this detection key.
    pub fn clue_key(&self) -> ClueKey {
        ClueKey(
            (self.dtk * decaf377::Element::GENERATOR)
                .vartime_compress()
                .0,
        )
    }

    /// Use this detection key to examine the given `clue`, returning `true` if the
    /// clue was possibly sent to this detection key's clue key.
    ///
    /// This test has false positives, but no false negatives.
    ///
    /// This function executes in constant time with respect to the detection
    /// key material, but short-circuits to return early on a false detection.
    #[allow(non_snake_case)]
    pub fn examine(&self, clue: &Clue) -> bool {
        let P_encoding = decaf377::Encoding::try_from(&clue.0[0..32]).expect("slice is right len");

        let P = if let Ok(P) = P_encoding.vartime_decompress() {
            P
        } else {
            // Invalid P encoding => not a match
            return false;
        };

        let y = if let Ok(y) =
            Fr::from_bytes_checked(&clue.0[32..64].try_into().expect("expected 32 bytes"))
        {
            y
        } else {
            // Invalid y encoding => not a match
            return false;
        };

        // Reject P = 0 or y = 0, as these never occur in well-formed clues; as
        // noted in the OpenPrivacy implementation, these could allow clues to
        // match any detection key.
        // https://docs.rs/fuzzytags/0.6.0/src/fuzzytags/lib.rs.html#348-351
        if P.is_identity() || y == Fr::ZERO {
            return false;
        }

        let precision_bits = match clue.precision() {
            Err(_) => return false,
            Ok(x) => x.bits() as u8,
        };
        let ciphertexts = BitSlice::<u8, order::Lsb0>::from_slice(&clue.0[65..68]);

        let m = hash::to_scalar(&P_encoding.0, precision_bits, &clue.0[65..68]);
        let Q_bytes = ((y * P) + (m * decaf377::Element::GENERATOR)).vartime_compress();

        for i in 0..(precision_bits as usize) {
            let Px_i = (P * self.xs[i]).vartime_compress();
            let key_i = hash::to_bit(&P_encoding.0, &Px_i.0, &Q_bytes.0);
            let msg_i = (ciphertexts[i] as u8) ^ key_i;
            // Short-circuit if we get a zero; this branch is dependent on the
            // ephemeral key bit `key_i`, not the long-term key `xs[i]`, so we
            // don't risk leaking any long-term secrets through timing channels.
            //
            // On the other hand, this gives a massive speedup, since we have a
            // 1/2 chance of rejecting after 1 iteration, 1/4 chance of
            // rejecting after 2 iterations, ..., so (in expectation) we do <= 2
            // iterations instead of n iterations.
            if msg_i == 0 {
                return false;
            }
        }

        // Otherwise, all message bits were 1 and we return true.
        true
    }
}