Skip to content

Commit 0e432fa

Browse files
authored
Merge pull request #82 from cipherstash/bug/divide-by-zero
2 parents fd4a248 + f8c6fe4 commit 0e432fa

File tree

8 files changed

+211
-24
lines changed

8 files changed

+211
-24
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ serde_json = "1.0.117"
3838
tracing-test = "0.2.5"
3939
# So we can get backtraces in tests
4040
miette = { version = "7.2.0", features = ["fancy"] }
41+
chrono = "0.4.38"
4142

4243
[features]
4344
default = ["tokio"]

src/crypto/sealed.rs

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ impl SealedTableEntry {
7070
///
7171
/// This should be used over [`Sealed::unseal`] when multiple values need to be unsealed.
7272
pub(crate) async fn unseal_all(
73-
items: Vec<SealedTableEntry>,
73+
items: Vec<Self>,
7474
spec: UnsealSpec<'_>,
7575
cipher: &Encryption<impl Credentials<Token = ServiceToken>>,
7676
) -> Result<Vec<Unsealed>, SealError> {
@@ -79,6 +79,11 @@ impl SealedTableEntry {
7979
sort_key_prefix,
8080
} = spec;
8181

82+
let items_len = items.len();
83+
if items_len == 0 {
84+
return Ok(Vec::new());
85+
}
86+
8287
let mut protected_items = {
8388
let capacity = items.len() * protected_attributes.len();
8489
FlattenedEncryptedAttributes::with_capacity(capacity)
@@ -98,16 +103,16 @@ impl SealedTableEntry {
98103
if protected_items.is_empty() {
99104
unprotected_items
100105
.into_iter()
101-
.map(|unprotected| {
102-
// TODO: Create a new_from_unprotected method
103-
Ok(Unsealed::new_from_parts(
104-
NormalizedProtectedAttributes::new(),
105-
unprotected,
106-
))
107-
})
106+
.map(|unprotected| Ok(Unsealed::new_from_unprotected(unprotected)))
108107
.collect()
109108
} else {
110-
let chunk_size = protected_items.len() / unprotected_items.len();
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+
))?;
111116

112117
protected_items
113118
.decrypt_all(cipher)
@@ -199,3 +204,52 @@ impl TryFrom<SealedTableEntry> for HashMap<String, AttributeValue> {
199204
Ok(map)
200205
}
201206
}
207+
208+
#[cfg(test)]
209+
mod 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+
// FIXME: Use the test cipher from CipherStash Client when that's ready
222+
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+
}

src/crypto/unsealed.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ impl Unsealed {
4040
}
4141
}
4242

43+
/// Create a new Unsealed from the protected and unprotected attributes.
44+
pub(crate) fn new_from_parts(
45+
protected: NormalizedProtectedAttributes,
46+
unprotected: TableAttributes,
47+
) -> Self {
48+
let mut unsealed = Self::new();
49+
unsealed.protected = protected;
50+
unsealed.unprotected = unprotected;
51+
unsealed
52+
}
53+
54+
/// Create a new Unsealed from the protected and unprotected attributes.
55+
pub(crate) fn new_from_unprotected(unprotected: TableAttributes) -> Self {
56+
let mut unsealed = Self::new();
57+
unsealed.unprotected = unprotected;
58+
unsealed
59+
}
60+
4361
#[deprecated(since = "0.7.3", note = "Use `Unsealed::take_unprotected` instead")]
4462
pub fn get_plaintext(&self, name: impl Into<AttributeName>) -> TableAttribute {
4563
self.unprotected
@@ -107,17 +125,6 @@ impl Unsealed {
107125
(self.protected.flatten(), self.unprotected)
108126
}
109127

110-
/// Create a new Unsealed from the protected and unprotected attributes.
111-
pub(crate) fn new_from_parts(
112-
protected: NormalizedProtectedAttributes,
113-
unprotected: TableAttributes,
114-
) -> Self {
115-
let mut unsealed = Self::new();
116-
unsealed.protected = protected;
117-
unsealed.unprotected = unprotected;
118-
unsealed
119-
}
120-
121128
/// Convert `self` into `T` using the attributes stored in `self`.
122129
/// The [Decryptable] trait must be implemented for `T` and this method calls [Decryptable::from_unsealed].
123130
pub fn into_value<T: Decryptable>(self) -> Result<T, SealError> {

src/encrypted_table/table_attribute.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub trait TryFromTableAttr: Sized {
1212
fn try_from_table_attr(value: TableAttribute) -> Result<Self, ReadConversionError>;
1313
}
1414

15-
#[derive(Clone, PartialEq)]
15+
#[derive(Clone, PartialEq, Debug)]
1616
pub enum TableAttribute {
1717
String(String),
1818
Number(String),

src/encrypted_table/table_attributes.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
collections::{hash_map::IntoIter, HashMap},
55
};
66

7-
#[derive(Clone)]
7+
#[derive(Clone, Debug)]
88
/// Represents a collection of attributes for a table entry.
99
/// Attributes are stored as a map of `String` to `TableAttribute`.
1010
pub struct TableAttributes(HashMap<AttributeName, TableAttribute>);
@@ -28,7 +28,8 @@ impl TableAttributes {
2828
value: impl Into<TableAttribute>,
2929
) {
3030
let name: AttributeName = name.into();
31-
self.0.insert(name, value.into());
31+
let attr: TableAttribute = value.into();
32+
self.0.insert(name, attr);
3233
}
3334

3435
/// Attempts to insert a value into a map with key `subkey` where the map is stored at `key`.
@@ -57,7 +58,6 @@ impl TableAttributes {
5758
let (protected, unprotected): (HashMap<_, _>, HashMap<_, _>) =
5859
self.0.into_iter().partition(|(k, _)| {
5960
let check = k.as_external_name();
60-
//protected_keys.iter().any(|key| match_key(check, key))
6161
protected_keys.iter().any(|key| check == key)
6262
});
6363

tests/allprotected_tests.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use chrono::NaiveDate;
2+
use cipherstash_dynamodb::{Decryptable, Encryptable, EncryptedTable, Identifiable, Searchable};
3+
use serial_test::serial;
4+
use std::future::Future;
5+
6+
mod common;
7+
8+
#[derive(
9+
Identifiable, Encryptable, Decryptable, Searchable, Debug, PartialEq, Ord, PartialOrd, Eq,
10+
)]
11+
#[cipherstash(sort_key_prefix = "user")]
12+
pub struct User {
13+
#[cipherstash(query = "exact", compound = "email#name")]
14+
#[cipherstash(query = "exact")]
15+
#[partition_key]
16+
pub email: String,
17+
18+
#[cipherstash(query = "prefix", compound = "email#name")]
19+
#[cipherstash(query = "prefix")]
20+
pub name: String,
21+
22+
pub dob: NaiveDate,
23+
}
24+
25+
impl User {
26+
pub fn new(email: impl Into<String>, name: impl Into<String>, dob: NaiveDate) -> Self {
27+
Self {
28+
name: name.into(),
29+
email: email.into(),
30+
dob,
31+
}
32+
}
33+
}
34+
35+
async fn run_test<F: Future<Output = ()>>(mut f: impl FnMut(EncryptedTable) -> F) {
36+
let config = aws_config::from_env()
37+
.endpoint_url("http://localhost:8000")
38+
.load()
39+
.await;
40+
41+
let client = aws_sdk_dynamodb::Client::new(&config);
42+
43+
let table_name = "test-users-plaintext-email";
44+
45+
common::create_table(&client, table_name).await;
46+
let table = EncryptedTable::init(client, table_name)
47+
.await
48+
.expect("Failed to init table");
49+
50+
table
51+
.put(User::new(
52+
"dan@coderdan.co",
53+
"Dan Draper",
54+
NaiveDate::from_ymd_opt(2000, 1, 10).unwrap(),
55+
))
56+
.await
57+
.expect("Failed to insert Dan");
58+
59+
table
60+
.put(User::new(
61+
"jane@smith.org",
62+
"Jane Smith",
63+
NaiveDate::from_ymd_opt(1990, 2, 20).unwrap(),
64+
))
65+
.await
66+
.expect("Failed to insert Jane");
67+
68+
table
69+
.put(User::new(
70+
"daniel@example.com",
71+
"Daniel Johnson",
72+
NaiveDate::from_ymd_opt(1980, 3, 30).unwrap(),
73+
))
74+
.await
75+
.expect("Failed to insert Daniel");
76+
77+
f(table).await;
78+
}
79+
80+
#[tokio::test]
81+
#[serial]
82+
async fn test_get() {
83+
run_test(|table| async move {
84+
let res: Option<User> = table
85+
.get("dan@coderdan.co")
86+
.await
87+
.expect("Failed to get user");
88+
89+
assert_eq!(
90+
res,
91+
Some(User::new(
92+
"dan@coderdan.co",
93+
"Dan Draper",
94+
NaiveDate::from_ymd_opt(2000, 1, 10).unwrap()
95+
))
96+
);
97+
})
98+
.await;
99+
}
100+
101+
#[tokio::test]
102+
#[serial]
103+
async fn test_query_single_exact() {
104+
run_test(|table| async move {
105+
let res: Vec<User> = table
106+
.query()
107+
.eq("email", "dan@coderdan.co")
108+
.send()
109+
.await
110+
.expect("Failed to query");
111+
112+
assert_eq!(
113+
res,
114+
vec![User::new(
115+
"dan@coderdan.co",
116+
"Dan Draper",
117+
NaiveDate::from_ymd_opt(2000, 1, 10).unwrap()
118+
)]
119+
);
120+
})
121+
.await;
122+
}
File renamed without changes.

0 commit comments

Comments
 (0)