cipherstash_dynamodb/traits/
mod.rs

1use crate::crypto::{SealError, Unsealed};
2pub use crate::encrypted_table::{TableAttribute, TryFromTableAttr};
3use cipherstash_client::encryption::EncryptionError;
4pub use cipherstash_client::{
5    credentials::{service_credentials::ServiceToken, Credentials},
6    encryption::{
7        compound_indexer::{
8            ComposableIndex, ComposablePlaintext, CompoundIndex, ExactIndex, PrefixIndex,
9        },
10        Encryption, Plaintext, PlaintextNullVariant, TryFromPlaintext,
11    },
12};
13
14mod primary_key;
15use miette::Diagnostic;
16pub use primary_key::*;
17
18use std::{
19    borrow::Cow,
20    fmt::{Debug, Display},
21};
22use thiserror::Error;
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum SingleIndex {
26    Exact,
27    Prefix,
28}
29
30impl Display for SingleIndex {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            Self::Exact => f.write_str("exact"),
34            Self::Prefix => f.write_str("prefix"),
35        }
36    }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum IndexType {
41    Single(SingleIndex),
42    Compound2((SingleIndex, SingleIndex)),
43}
44
45impl Display for IndexType {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::Single(index) => Display::fmt(index, f),
49            Self::Compound2((index_a, index_b)) => {
50                Display::fmt(index_a, f)?;
51                f.write_str(":")?;
52                Display::fmt(index_b, f)?;
53                Ok(())
54            }
55        }
56    }
57}
58
59#[derive(Debug, Error, Diagnostic)]
60pub enum ReadConversionError {
61    #[error("Missing attribute: {0}")]
62    NoSuchAttribute(String),
63    #[error("Invalid format: {0}")]
64    InvalidFormat(String),
65    #[error("Failed to convert attribute: {0} from Plaintext")]
66    ConversionFailed(String),
67}
68
69#[derive(Debug, Error)]
70pub enum WriteConversionError {
71    #[error("Failed to convert attribute: '{0}' to Plaintext")]
72    ConversionFailed(String),
73}
74
75#[derive(Error, Debug)]
76pub enum PrimaryKeyError {
77    #[error("EncryptionError: {0}")]
78    EncryptionError(#[from] EncryptionError),
79    #[error("PrimaryKeyError: {0}")]
80    Unknown(String),
81}
82
83pub trait Identifiable {
84    type PrimaryKey: PrimaryKey;
85
86    fn get_primary_key(&self) -> Self::PrimaryKey;
87
88    fn is_sk_encrypted() -> bool {
89        false
90    }
91
92    fn is_pk_encrypted() -> bool {
93        false
94    }
95
96    fn type_name() -> Cow<'static, str>;
97    fn sort_key_prefix() -> Option<Cow<'static, str>>;
98}
99
100pub trait Encryptable: Debug + Sized + Identifiable {
101    /// Defines what attributes are protected and should be encrypted for this type.
102    ///
103    /// Must be equal to or a superset of protected_attributes on the [`Decryptable`] type.
104    fn protected_attributes() -> Cow<'static, [Cow<'static, str>]>;
105
106    /// Defines what attributes are plaintext for this type.
107    ///
108    /// Must be equal to or a superset of plaintext_attributes on the [`Decryptable`] type.
109    fn plaintext_attributes() -> Cow<'static, [Cow<'static, str>]>;
110
111    fn into_unsealed(self) -> Unsealed;
112}
113
114pub trait Searchable: Encryptable {
115    fn attribute_for_index(
116        &self,
117        _index_name: &str,
118        _index_type: IndexType,
119    ) -> Option<ComposablePlaintext> {
120        None
121    }
122
123    // TODO: Make a type to represent the result of this function
124    /// Returns of indexes with their name and type.
125    fn protected_indexes() -> Cow<'static, [(Cow<'static, str>, IndexType)]> {
126        Cow::Borrowed(&[])
127    }
128
129    fn index_by_name(
130        _index_name: &str,
131        _index_type: IndexType,
132    ) -> Option<Box<dyn ComposableIndex + Send>> {
133        None
134    }
135}
136
137pub trait Decryptable: Sized {
138    /// Convert an `Unsealed` into a `Self`.
139    fn from_unsealed(unsealed: Unsealed) -> Result<Self, SealError>;
140
141    /// Defines what attributes are protected and decryptable for this type.
142    ///
143    /// Must be equal to or a subset of protected_attributes on the [`Encryptable`] type.
144    fn protected_attributes() -> Cow<'static, [Cow<'static, str>]>;
145
146    /// Defines what attributes are plaintext for this type.
147    ///
148    /// Must be equal to or a subset of protected_attributes on the [`Encryptable`] type.
149    fn plaintext_attributes() -> Cow<'static, [Cow<'static, str>]>;
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use miette::IntoDiagnostic;
156    use std::collections::BTreeMap;
157
158    fn make_btree_map() -> BTreeMap<String, String> {
159        let mut map = BTreeMap::new();
160        map.insert("a".to_string(), "value-a".to_string());
161        map.insert("b".to_string(), "value-b".to_string());
162        map.insert("c".to_string(), "value-c".to_string());
163        map
164    }
165
166    #[derive(Debug, Clone, PartialEq)]
167    struct Test {
168        pub id: String,
169        pub name: String,
170        pub age: i16,
171        pub tag: String,
172        pub attrs: BTreeMap<String, String>,
173    }
174
175    impl Identifiable for Test {
176        type PrimaryKey = Pk;
177
178        fn get_primary_key(&self) -> Self::PrimaryKey {
179            Pk(self.id.to_string())
180        }
181        #[inline]
182        fn type_name() -> Cow<'static, str> {
183            std::borrow::Cow::Borrowed("test")
184        }
185        #[inline]
186        fn sort_key_prefix() -> Option<Cow<'static, str>> {
187            None
188        }
189        fn is_pk_encrypted() -> bool {
190            true
191        }
192        fn is_sk_encrypted() -> bool {
193            false
194        }
195    }
196
197    fn put_attrs(unsealed: &mut Unsealed, attrs: BTreeMap<String, String>) {
198        attrs.into_iter().for_each(|(k, v)| {
199            unsealed.add_protected_map_field("attrs", k, Plaintext::from(v));
200        })
201    }
202
203    impl Encryptable for Test {
204        fn protected_attributes() -> Cow<'static, [Cow<'static, str>]> {
205            Cow::Borrowed(&[Cow::Borrowed("name")])
206        }
207
208        fn plaintext_attributes() -> Cow<'static, [Cow<'static, str>]> {
209            Cow::Borrowed(&[Cow::Borrowed("age")])
210        }
211
212        fn into_unsealed(self) -> Unsealed {
213            let mut unsealed = Unsealed::new_with_descriptor(<Self as Identifiable>::type_name());
214            unsealed.add_protected("id", self.id);
215            unsealed.add_protected("name", self.name);
216            unsealed.add_protected("age", self.age);
217            unsealed.add_unprotected("tag", self.tag);
218            put_attrs(&mut unsealed, self.attrs);
219            unsealed
220        }
221    }
222
223    // TODO: Make this return an error that we we can expose to users
224    fn get_attrs<T>(unsealed: &mut Unsealed) -> Result<T, SealError>
225    where
226        T: FromIterator<(String, String)>,
227    {
228        unsealed
229            .take_protected_map("attrs")
230            .ok_or(SealError::MissingAttribute("attrs".to_string()))?
231            .into_iter()
232            .map(|(k, v)| {
233                TryFromPlaintext::try_from_plaintext(v)
234                    .map(|v| (k, v))
235                    .map_err(SealError::from)
236            })
237            .collect()
238    }
239
240    // TODO: Test this with struct fields called pk and sk
241    impl Decryptable for Test {
242        fn from_unsealed(mut unsealed: Unsealed) -> Result<Self, SealError> {
243            Ok(Self {
244                id: TryFromPlaintext::try_from_optional_plaintext(unsealed.take_protected("id"))?,
245                name: TryFromPlaintext::try_from_optional_plaintext(
246                    unsealed.take_protected("name"),
247                )?,
248                age: TryFromPlaintext::try_from_optional_plaintext(unsealed.take_protected("age"))?,
249                tag: TryFromTableAttr::try_from_table_attr(unsealed.take_unprotected("tag"))?,
250                attrs: get_attrs(&mut unsealed)?,
251            })
252        }
253
254        // FIXME: create a card: this API is brittle because this function must match the from_unsealed function behavior
255        // The same is true between this and the Encryptable trait
256        fn protected_attributes() -> Cow<'static, [Cow<'static, str>]> {
257            Cow::Borrowed(&[
258                Cow::Borrowed("name"),
259                Cow::Borrowed("age"),
260                Cow::Borrowed("attrs"),
261            ])
262        }
263
264        fn plaintext_attributes() -> Cow<'static, [Cow<'static, str>]> {
265            Cow::Borrowed(&[Cow::Borrowed("tag")])
266        }
267    }
268
269    #[test]
270    fn test_encryptable() -> Result<(), Box<dyn std::error::Error>> {
271        let test = Test {
272            id: "id-100".to_string(),
273            name: "name".to_string(),
274            tag: "tag".to_string(),
275            age: 42,
276            attrs: make_btree_map(),
277        };
278
279        let unsealed = test.clone().into_unsealed();
280        assert_eq!(test, Test::from_unsealed(unsealed).into_diagnostic()?);
281
282        Ok(())
283    }
284}