enquo_core/datatype/boolean/
v1.rs

1//! Our v1 boolean
2//!
3
4use ciborium::{cbor, value::Value};
5use serde::{Deserialize, Serialize};
6use serde_with::skip_serializing_none;
7use std::cmp::Ordering;
8
9use crate::{
10    crypto::{AES256v1, OREv1},
11    field::KeyId,
12    Error, Field,
13};
14
15/// Das Bool
16#[skip_serializing_none]
17#[derive(Debug, Serialize, Deserialize)]
18#[doc(hidden)]
19pub struct V1 {
20    /// The encrypted value
21    #[serde(rename = "a")]
22    aes_ciphertext: AES256v1,
23    /// The queryable form of the encrypted value
24    #[serde(rename = "o")]
25    ore_ciphertext: Option<OREv1<1, 2>>,
26    /// A serialisation-friendly format of the field key ID
27    #[serde(rename = "k", with = "serde_bytes")]
28    kid: Vec<u8>,
29}
30
31/// The identifier for the subkey that produces the ORE ciphertext
32#[allow(non_upper_case_globals)]
33const BOOLEANv1_ORE_KEY_IDENTIFIER: &[u8] = b"boolean::V1.ore_key";
34
35impl V1 {
36    /// Make the encrypted boolean
37    pub(crate) fn new(b: bool, context: &[u8], field: &Field) -> Result<V1, Error> {
38        Self::encrypt(b, context, field, false)
39    }
40
41    /// Make the encrypted boolean, with reduced security guarantees
42    pub(crate) fn new_with_unsafe_parts(
43        b: bool,
44        context: &[u8],
45        field: &Field,
46    ) -> Result<V1, Error> {
47        Self::encrypt(b, context, field, true)
48    }
49
50    /// Do the hard yards of generating the ciphertexts and assembling the struct
51    fn encrypt(b: bool, context: &[u8], field: &Field, include_left: bool) -> Result<V1, Error> {
52        let v = cbor!(b).map_err(|e| {
53            Error::EncodingError(format!("failed to convert bool to ciborium value: {e}"))
54        })?;
55
56        let mut msg: Vec<u8> = Default::default();
57        ciborium::ser::into_writer(&v, &mut msg)
58            .map_err(|e| Error::EncodingError(format!("failed to encode bool value: {e}")))?;
59
60        let aes = AES256v1::new(&msg, context, field)?;
61
62        let ore = if include_left {
63            OREv1::<1, 2>::new_with_left(b, BOOLEANv1_ORE_KEY_IDENTIFIER, field)?
64        } else {
65            OREv1::<1, 2>::new(b, BOOLEANv1_ORE_KEY_IDENTIFIER, field)?
66        };
67
68        Ok(V1 {
69            aes_ciphertext: aes,
70            ore_ciphertext: Some(ore),
71            kid: field.key_id()?.into(),
72        })
73    }
74
75    /// Extract a plaintext bool
76    pub(crate) fn decrypt(&self, context: &[u8], field: &Field) -> Result<bool, Error> {
77        let pt = self.aes_ciphertext.decrypt(context, field)?;
78
79        let v = ciborium::de::from_reader(&*pt)
80            .map_err(|e| Error::DecodingError(format!("could not decode decrypted value: {e}")))?;
81
82        #[allow(clippy::wildcard_enum_match_arm)] // that is, indeed, exactly what I want here
83        match v {
84            Value::Bool(b) => Ok(b),
85            _ => Err(Error::DecodingError(format!(
86                "Decoded value is not a boolean (got {v:?})"
87            ))),
88        }
89    }
90
91    /// Return the field key ID in canonical form
92    pub(crate) fn key_id(&self) -> KeyId {
93        let mut key_id: KeyId = Default::default();
94        key_id.copy_from_slice(&self.kid);
95        key_id
96    }
97
98    /// Remove the "queryable" from "queryable encrypted boolean"
99    pub(crate) fn make_unqueryable(&mut self) {
100        self.ore_ciphertext = None;
101    }
102}
103
104impl Ord for V1 {
105    #[allow(clippy::panic, clippy::expect_used)] // No way to signal error from impl Ord
106    fn cmp(&self, other: &Self) -> Ordering {
107        assert!(
108            self.kid == other.kid,
109            "Cannot compare ciphertexts from different keys"
110        );
111
112        let lhs = self
113            .ore_ciphertext
114            .as_ref()
115            .expect("Cannot compare without an ORE ciphertext on the left-hand side");
116        let rhs = other
117            .ore_ciphertext
118            .as_ref()
119            .expect("Cannot compare without an ORE ciphertext on the right-hand side");
120
121        lhs.cmp(rhs)
122    }
123}
124
125impl PartialOrd for V1 {
126    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
127        Some(self.cmp(other))
128    }
129}
130
131impl PartialEq for V1 {
132    fn eq(&self, other: &Self) -> bool {
133        self.cmp(other) == Ordering::Equal
134    }
135}
136
137impl Eq for V1 {}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::{key_provider::Static, Root};
143    use std::sync::Arc;
144
145    fn field() -> Field {
146        Root::new(Arc::new(
147            Static::new(b"this is a suuuuper long test key").unwrap(),
148        ))
149        .unwrap()
150        .field(b"foo", b"bar")
151        .unwrap()
152    }
153
154    #[test]
155    fn value_round_trips() {
156        let true_value = V1::new(true, b"context", &field()).unwrap();
157        let false_value = V1::new(false, b"context", &field()).unwrap();
158
159        assert_eq!(true, true_value.decrypt(b"context", &field()).unwrap());
160        assert_eq!(false, false_value.decrypt(b"context", &field()).unwrap());
161    }
162
163    #[test]
164    fn incorrect_context_fails() {
165        let value = V1::new(true, b"somecontext", &field()).unwrap();
166
167        let err = value.decrypt(b"othercontext", &field()).err();
168        assert!(matches!(err, Some(Error::DecryptionError(_))));
169    }
170
171    #[test]
172    fn serialised_ciphertext_size() {
173        let value = V1::new(true, b"somecontext", &field()).unwrap();
174        let serde_value = cbor!(value).unwrap();
175
176        let mut s: Vec<u8> = vec![];
177        ciborium::ser::into_writer(&serde_value, &mut s).unwrap();
178        assert!(s.len() < 81, "s.len() == {}", s.len());
179    }
180
181    #[test]
182    fn default_encryption_is_safe() {
183        let value = V1::new(true, b"somecontext", &field()).unwrap();
184
185        assert!(!value.ore_ciphertext.unwrap().has_left());
186    }
187}