cipherstash_dynamodb/crypto/
sealed.rs1use crate::{
2 crypto::attrs::FlattenedEncryptedAttributes,
3 encrypted_table::TableEntry,
4 traits::{ReadConversionError, WriteConversionError},
5 Decryptable, Identifiable,
6};
7use aws_sdk_dynamodb::{primitives::Blob, types::AttributeValue};
8use cipherstash_client::{
9 credentials::{service_credentials::ServiceToken, Credentials},
10 encryption::Encryption,
11};
12use itertools::Itertools;
13use std::{borrow::Cow, collections::HashMap};
14
15use super::{attrs::NormalizedProtectedAttributes, SealError, Unsealed};
16
17pub struct SealedTableEntry(pub(super) TableEntry);
20
21pub struct UnsealSpec<'a> {
22 pub(crate) protected_attributes: Cow<'a, [Cow<'a, str>]>,
23
24 pub(crate) sort_key_prefix: String,
30}
31
32impl UnsealSpec<'static> {
33 pub fn new_for_decryptable<D>() -> Self
34 where
35 D: Decryptable + Identifiable,
36 {
37 Self {
38 protected_attributes: D::protected_attributes(),
39 sort_key_prefix: D::sort_key_prefix()
40 .as_deref()
41 .map(ToOwned::to_owned)
42 .unwrap_or(D::type_name().to_string()),
43 }
44 }
45}
46
47impl SealedTableEntry {
48 pub fn vec_from<O: TryInto<Self>>(
49 items: impl IntoIterator<Item = O>,
50 ) -> Result<Vec<Self>, <O as TryInto<Self>>::Error> {
51 items.into_iter().map(Self::from_inner).collect()
52 }
53
54 pub(super) fn from_inner<O: TryInto<Self>>(
55 item: O,
56 ) -> Result<Self, <O as TryInto<Self>>::Error> {
57 item.try_into()
58 }
59
60 pub(crate) fn inner(&self) -> &TableEntry {
61 &self.0
62 }
63
64 pub(crate) fn into_inner(self) -> TableEntry {
65 self.0
66 }
67
68 pub(crate) async fn unseal_all(
73 items: Vec<Self>,
74 spec: UnsealSpec<'_>,
75 cipher: &Encryption<impl Credentials<Token = ServiceToken>>,
76 ) -> Result<Vec<Unsealed>, SealError> {
77 let UnsealSpec {
78 protected_attributes,
79 sort_key_prefix,
80 } = spec;
81
82 let items_len = items.len();
83 if items_len == 0 {
84 return Ok(Vec::new());
85 }
86
87 let mut protected_items = {
88 let capacity = items.len() * protected_attributes.len();
89 FlattenedEncryptedAttributes::with_capacity(capacity)
90 };
91 let mut unprotected_items = Vec::with_capacity(items.len());
92
93 for item in items.into_iter() {
94 let (protected, unprotected) = item
95 .into_inner()
96 .attributes
97 .partition(protected_attributes.as_ref());
98
99 protected_items.try_extend(protected, sort_key_prefix.clone())?;
100 unprotected_items.push(unprotected);
101 }
102
103 if protected_items.is_empty() {
104 unprotected_items
105 .into_iter()
106 .map(|unprotected| Ok(Unsealed::new_from_unprotected(unprotected)))
107 .collect()
108 } else {
109 let chunk_size =
110 protected_items
111 .len()
112 .checked_div(items_len)
113 .ok_or(SealError::AssertionFailed(
114 "Division by zero when calculating chunk size".to_string(),
115 ))?;
116
117 protected_items
118 .decrypt_all(cipher)
119 .await?
120 .into_iter()
121 .chunks(chunk_size)
123 .into_iter()
124 .map(|fpa| fpa.into_iter().collect::<NormalizedProtectedAttributes>())
125 .zip_eq(unprotected_items.into_iter())
126 .map(|(fpa, unprotected)| Ok(Unsealed::new_from_parts(fpa, unprotected)))
127 .collect()
128 }
129 }
130
131 pub(crate) async fn unseal(
135 self,
136 spec: UnsealSpec<'_>,
137 cipher: &Encryption<impl Credentials<Token = ServiceToken>>,
138 ) -> Result<Unsealed, SealError> {
139 let mut vec = Self::unseal_all(vec![self], spec, cipher).await?;
140
141 if vec.len() != 1 {
142 let actual = vec.len();
143
144 return Err(SealError::AssertionFailed(format!(
145 "Expected unseal_all to return 1 result but got {actual}"
146 )));
147 }
148
149 Ok(vec.remove(0))
150 }
151}
152
153impl TryFrom<HashMap<String, AttributeValue>> for SealedTableEntry {
154 type Error = ReadConversionError;
155
156 fn try_from(item: HashMap<String, AttributeValue>) -> Result<Self, Self::Error> {
157 let pk = item
158 .get("pk")
159 .ok_or(ReadConversionError::NoSuchAttribute("pk".to_string()))?
160 .as_s()
161 .map_err(|_| ReadConversionError::InvalidFormat("pk".to_string()))?
162 .to_string();
163
164 let sk = item
165 .get("sk")
166 .ok_or(ReadConversionError::NoSuchAttribute("sk".to_string()))?
167 .as_s()
168 .map_err(|_| ReadConversionError::InvalidFormat("sk".to_string()))?
169 .to_string();
170
171 let mut table_entry = TableEntry::new(pk, sk);
172
173 item.into_iter()
177 .filter(|(k, _)| k != "pk" && k != "sk" && k != "term")
178 .for_each(|(k, v)| {
179 table_entry.add_attribute(k, v.into());
180 });
181
182 Ok(SealedTableEntry(table_entry))
183 }
184}
185
186impl TryFrom<SealedTableEntry> for HashMap<String, AttributeValue> {
188 type Error = WriteConversionError;
189
190 fn try_from(item: SealedTableEntry) -> Result<Self, Self::Error> {
191 let mut map = HashMap::new();
192
193 map.insert("pk".to_string(), AttributeValue::S(item.0.pk));
194 map.insert("sk".to_string(), AttributeValue::S(item.0.sk));
195
196 if let Some(term) = item.0.term {
197 map.insert("term".to_string(), AttributeValue::B(Blob::new(term)));
198 }
199
200 item.0.attributes.into_iter().for_each(|(k, v)| {
201 map.insert(k.into_stored_name(), v.into());
202 });
203
204 Ok(map)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::SealedTableEntry;
211 use cipherstash_client::{
212 credentials::{auto_refresh::AutoRefresh, service_credentials::ServiceCredentials},
213 encryption::Encryption,
214 ConsoleConfig, ZeroKMS, ZeroKMSConfig,
215 };
216 use miette::IntoDiagnostic;
217 use std::borrow::Cow;
218
219 type Cipher = Encryption<AutoRefresh<ServiceCredentials>>;
220
221 async fn get_cipher() -> Result<Cipher, Box<dyn std::error::Error>> {
223 let console_config = ConsoleConfig::builder().with_env().build()?;
224 let zero_kms_config = ZeroKMSConfig::builder()
225 .decryption_log(true)
226 .with_env()
227 .console_config(&console_config)
228 .build_with_client_key()?;
229
230 let zero_kms_client = ZeroKMS::new_with_client_key(
231 &zero_kms_config.base_url(),
232 AutoRefresh::new(zero_kms_config.credentials()),
233 zero_kms_config.decryption_log_path().as_deref(),
234 zero_kms_config.client_key(),
235 );
236
237 let config = zero_kms_client.load_dataset_config().await?;
238 Ok(Encryption::new(config.index_root_key, zero_kms_client))
239 }
240
241 #[tokio::test]
242 async fn test_unseal_all_empty() -> Result<(), Box<dyn std::error::Error>> {
243 let spec = super::UnsealSpec {
244 protected_attributes: Cow::Borrowed(&[]),
245 sort_key_prefix: "test".to_string(),
246 };
247 let cipher = get_cipher().await?;
248 let results = SealedTableEntry::unseal_all(vec![], spec, &cipher)
249 .await
250 .into_diagnostic()?;
251 assert!(results.is_empty());
252
253 Ok(())
254 }
255}