11use aws_sdk_dynamodb:: { primitives:: Blob , types:: AttributeValue } ;
2- use cipherstash_client:: encryption:: {
3- compound_indexer:: { ComposableIndex , ComposablePlaintext } ,
4- Plaintext ,
2+ use cipherstash_client:: {
3+ credentials:: { service_credentials:: ServiceToken , Credentials } ,
4+ encryption:: {
5+ compound_indexer:: { ComposableIndex , ComposablePlaintext } ,
6+ Encryption , Plaintext ,
7+ } ,
58} ;
69use itertools:: Itertools ;
7- use std:: marker:: PhantomData ;
10+ use std:: { collections :: HashMap , marker:: PhantomData } ;
811
912use crate :: {
1013 traits:: { Decryptable , Searchable } ,
@@ -26,6 +29,71 @@ pub struct QueryBuilder<'t, S, D = Dynamo> {
2629 __table : PhantomData < S > ,
2730}
2831
32+ pub struct PreparedQuery {
33+ index_name : String ,
34+ type_name : String ,
35+ composed_index : Box < dyn ComposableIndex > ,
36+ plaintext : ComposablePlaintext ,
37+ }
38+
39+ impl PreparedQuery {
40+ pub async fn encrypt (
41+ self ,
42+ cipher : & Encryption < impl Credentials < Token = ServiceToken > > ,
43+ ) -> Result < AttributeValue , QueryError > {
44+ let PreparedQuery {
45+ index_name,
46+ composed_index,
47+ plaintext,
48+ type_name,
49+ } = self ;
50+
51+ let index_term = cipher. compound_query (
52+ & CompoundIndex :: new ( composed_index) ,
53+ plaintext,
54+ Some ( format ! ( "{}#{}" , type_name, index_name) ) ,
55+ 12 ,
56+ ) ?;
57+
58+ // With DynamoDB queries must always return a single term
59+ let term = if let IndexTerm :: Binary ( x) = index_term {
60+ AttributeValue :: B ( Blob :: new ( x) )
61+ } else {
62+ Err ( QueryError :: Other ( format ! (
63+ "Returned IndexTerm had invalid type: {index_term:?}"
64+ ) ) ) ?
65+ } ;
66+
67+ Ok ( term)
68+ }
69+
70+ pub async fn send (
71+ self ,
72+ table : & EncryptedTable < Dynamo > ,
73+ ) -> Result < Vec < HashMap < String , AttributeValue > > , QueryError > {
74+ let term = self . encrypt ( & table. cipher ) . await ?;
75+
76+ let query = table
77+ . db
78+ . query ( )
79+ . table_name ( & table. db . table_name )
80+ . index_name ( "TermIndex" )
81+ . key_condition_expression ( "term = :term" )
82+ . expression_attribute_values ( ":term" , term) ;
83+
84+ let result = query
85+ . send ( )
86+ . await
87+ . map_err ( |e| QueryError :: AwsError ( format ! ( "{e:?}" ) ) ) ?;
88+
89+ let items = result
90+ . items
91+ . ok_or_else ( || QueryError :: AwsError ( "Expected items entry on aws response" . into ( ) ) ) ?;
92+
93+ Ok ( items)
94+ }
95+ }
96+
2997impl < ' t , S , D > QueryBuilder < ' t , S , D >
3098where
3199 S : Searchable ,
50118 self
51119 }
52120
53- fn build (
54- self ,
55- ) -> Result < ( String , Box < dyn ComposableIndex > , ComposablePlaintext , Self ) , QueryError > {
121+ fn build ( self ) -> Result < PreparedQuery , QueryError > {
56122 let items_len = self . parts . len ( ) ;
57123
58124 // this is the simplest way to brute force the index names but relies on some gross
61127 let ( indexes, plaintexts) : ( Vec < ( & String , & SingleIndex ) > , Vec < & Plaintext > ) =
62128 perm. into_iter ( ) . map ( |x| ( ( & x. 0 , & x. 1 ) , & x. 2 ) ) . unzip ( ) ;
63129
64- let name = indexes. iter ( ) . map ( |( index_name, _) | index_name) . join ( "#" ) ;
130+ let index_name = indexes. iter ( ) . map ( |( index_name, _) | index_name) . join ( "#" ) ;
65131
66132 let mut indexes_iter = indexes. iter ( ) . map ( |( _, index) | * * index) ;
67133
92158 }
93159 } ;
94160
95- if let Some ( index ) = S :: index_by_name ( name . as_str ( ) , index_type) {
161+ if let Some ( composed_index ) = S :: index_by_name ( index_name . as_str ( ) , index_type) {
96162 let mut plaintext = ComposablePlaintext :: new ( plaintexts[ 0 ] . clone ( ) ) ;
97163
98164 for p in plaintexts[ 1 ..] . iter ( ) {
@@ -101,7 +167,12 @@ where
101167 . expect ( "Failed to compose" ) ;
102168 }
103169
104- return Ok ( ( name, index, plaintext, self ) ) ;
170+ return Ok ( PreparedQuery {
171+ index_name,
172+ type_name : S :: type_name ( ) . to_string ( ) ,
173+ plaintext,
174+ composed_index,
175+ } ) ;
105176 }
106177 }
107178
@@ -118,43 +189,11 @@ where
118189 S : Searchable + Identifiable ,
119190{
120191 pub async fn load < T : Decryptable > ( self ) -> Result < Vec < T > , QueryError > {
121- let ( index_name, index, plaintext, builder) = self . build ( ) ?;
122-
123- let index_term = builder. table . cipher . compound_query (
124- & CompoundIndex :: new ( index) ,
125- plaintext,
126- Some ( format ! ( "{}#{}" , S :: type_name( ) , index_name) ) ,
127- 12 ,
128- ) ?;
129-
130- // With DynamoDB queries must always return a single term
131- let term = if let IndexTerm :: Binary ( x) = index_term {
132- AttributeValue :: B ( Blob :: new ( x) )
133- } else {
134- Err ( QueryError :: Other ( format ! (
135- "Returned IndexTerm had invalid type: {index_term:?}"
136- ) ) ) ?
137- } ;
138-
139- let query = builder
140- . table
141- . db
142- . query ( )
143- . table_name ( & builder. table . db . table_name )
144- . index_name ( "TermIndex" )
145- . key_condition_expression ( "term = :term" )
146- . expression_attribute_values ( ":term" , term) ;
147-
148- let result = query
149- . send ( )
150- . await
151- . map_err ( |e| QueryError :: AwsError ( format ! ( "{e:?}" ) ) ) ?;
152-
153- let items = result
154- . items
155- . ok_or_else ( || QueryError :: AwsError ( "Expected items entry on aws response" . into ( ) ) ) ?;
192+ let table = self . table ;
193+ let query = self . build ( ) ?;
156194
157- let results = builder. table . decrypt_all ( items) . await ?;
195+ let items = query. send ( table) . await ?;
196+ let results = table. decrypt_all ( items) . await ?;
158197
159198 Ok ( results)
160199 }
0 commit comments