@@ -51,10 +51,21 @@ Upload it to ZeroKMS using the following command:
51
51
52
52
---
53
53
54
- ### Setup DynamoDB
54
+ <!-- cargo-rdme start -->
55
+
56
+ ###### Cryptonamo: Encrypted Tables for DynamoDB
57
+
58
+ Based on the CipherStash SDK and ZeroKMS key service, Cryptonamo provides a simple interface for
59
+ storing and retrieving encrypted data in DynamoDB.
60
+
61
+ ###### Usage
55
62
56
63
To use Cryptonamo, you must first create a table in DynamoDB.
57
- The table must have a primary key and sort key, both of type String.
64
+ The table must have a at least partition key, sort key, and term field - all of type String.
65
+
66
+ Cryptonamo also expects a Global Secondary Index called "TermIndex" to exist if you want to
67
+ search and query against records. This index should project all fields and have a key schema
68
+ that is a hash on the term attribute.
58
69
59
70
You can use the the ` aws ` CLI to create a table with an appropriate schema as follows:
60
71
@@ -69,44 +80,45 @@ aws dynamodb create-table \
69
80
AttributeName=pk,KeyType=HASH \
70
81
AttributeName=sk,KeyType=RANGE \
71
82
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
72
- --global-secondary-indexes " IndexName=TermIndex,KeySchema=[{AttributeName=term,KeyType=HASH},{AttributeName=pk,KeyType=RANGE} ],Projection={ProjectionType=ALL},ProvisionedThroughput={ReadCapacityUnits=5,WriteCapacityUnits=5}"
83
+ --global-secondary-indexes " IndexName=TermIndex,KeySchema=[{AttributeName=term,KeyType=HASH}],Projection={ProjectionType=ALL},ProvisionedThroughput={ReadCapacityUnits=5,WriteCapacityUnits=5}"
73
84
```
74
85
75
86
See below for more information on schema design for Cryptonamo tables.
76
87
77
- #### Annotating a Cryptanomo Type
88
+ ####### Annotating a Cryptanomo Type
78
89
79
- To use Cryptonamo, you must first annotate a struct with the the derive macros for the Cryptonamo traits you wish to implement.
90
+ To use Cryptonamo, you must first annotate a struct with the ` Encryptable ` derive macro, as
91
+ well as the ` Searchable ` and ` Decryptable ` macros if you want to support those features.
80
92
81
93
``` rust
82
- use cryptonamo :: {Encryptable , Decryptable , Searchable };
94
+ use cryptonamo :: {Searchable , Decryptable , Encryptable };
83
95
84
- #[derive(Debug , Encryptable , Decryptable , Searchable )]
96
+ #[derive(Debug , Searchable , Decryptable , Encryptable )]
85
97
struct User {
86
98
name : String ,
87
-
88
99
#[partition_key]
89
100
email : String ,
90
101
}
91
102
```
92
103
93
- This example implements the traits:
104
+ These derive macros will generate implementations for the following traits of the same name :
94
105
95
- * ` Decryptable ` - a trait that allows you to decrypt the record from DynamoDB
96
- * ` Encryptable ` - a trait that allows you to encrypt the record for storage in DynamoDB
106
+ * ` Decryptable ` - a trait that allows you to decrypt a record from DynamoDB
107
+ * ` Encryptable ` - a trait that allows you to encrypt a record for storage in DynamoDB
97
108
* ` Searchable ` - a trait that allows you to search for records in DynamoDB
98
109
99
110
The above example is the minimum required to use Cryptonamo however you can expand capabilities via several macros.
100
111
101
- #### Controlling Encryption
112
+ ####### Controlling Encryption
102
113
103
- By default, all fields on a ` Cryptanomo ` type are encrypted and stored in the index.
104
- To store a field as a plaintext, use the ` plaintext ` attribute:
114
+ By default, all fields on an annotated struct are stored encrypted in the table.
115
+
116
+ To store a field as a plaintext, you can use the ` plaintext ` attribute:
105
117
106
118
``` rust
107
- use cryptonamo :: Cryptonamo ;
119
+ use cryptonamo :: { Searchable , Decryptable , Encryptable } ;
108
120
109
- #[derive(Cryptonamo )]
121
+ #[derive(Debug , Searchable , Decryptable , Encryptable )]
110
122
struct User {
111
123
#[partition_key]
112
124
email : String ,
@@ -117,22 +129,34 @@ struct User {
117
129
}
118
130
```
119
131
120
- Most basic rust types will work automatically but you can implement a conversion trait for [ Plaintext ] to support custom types .
132
+ If you don't want a field stored in the the database at all, you can annotate the field with ` #[cryptonamo(skip)] ` .
121
133
122
134
``` rust
123
- impl From <MyType > for Plaintext {
124
- fn from (t : MyType ) -> Self {
125
- t . as_bytes (). into ()
126
- }
135
+ use cryptonamo :: {Searchable , Encryptable , Decryptable };
136
+
137
+ #[derive(Debug , Searchable , Encryptable , Decryptable )]
138
+ struct User {
139
+ #[partition_key]
140
+ email : String ,
141
+ name : String ,
142
+
143
+ #[cryptonamo(skip)]
144
+ not_required : String ,
127
145
}
128
146
```
129
147
130
- If you don't want a field stored in the the database at all, you can annotate the field with ` #[cryptonamo(skip)] ` .
148
+ If you implement the ` Decryptable ` trait these skipped fields need to implement ` Default ` .
149
+
150
+ ####### Sort keys
151
+
152
+ Cryptanomo requires every record to have a sort key. By default this will be derived based on the name of the struct.
153
+ However, if you want to specify your own, you can use the ` sort_key_prefix ` attribute:
131
154
132
155
``` rust
133
- use cryptonamo :: Cryptonamo ;
156
+ use cryptonamo :: Encryptable ;
134
157
135
- #[derive(Cryptonamo )]
158
+ #[derive(Debug , Encryptable )]
159
+ #[cryptonamo(sort_key_prefix = " user" )]
136
160
struct User {
137
161
#[partition_key]
138
162
email : String ,
@@ -143,68 +167,93 @@ struct User {
143
167
}
144
168
```
145
169
146
- #### Sort keys
170
+ ######## Dynamic Sort keys
147
171
148
- Cryptanomo requires every record to have a sort key and it derives it automatically based on the name of the struct.
149
- However, if you want to specify your own, you can use the ` sort_key_prefix ` attribute:
172
+ Cryptonamo also supports specifying the sort key dynamically based on a field on the struct.
173
+ You can choose the field using the ` #[sort_key] ` attribute.
150
174
151
175
``` rust
152
- use cryptonamo :: Cryptonamo ;
176
+ use cryptonamo :: Encryptable ;
153
177
154
- #[derive(Cryptonamo )]
155
- #[cryptonamo(partition_key = " email" )]
156
- #[cryptonamo(sort_key_prefix = " user" )]
178
+ #[derive(Debug , Encryptable )]
157
179
struct User {
180
+ #[partition_key]
181
+ email : String ,
182
+ #[sort_key]
158
183
name : String ,
159
184
160
185
#[cryptonamo(skip)]
161
186
not_required : String ,
162
187
}
163
188
```
164
- Note that you can ` skip ` the partition key as well.
165
- In this case, the data won't be stored as an attribute table but a hash of the value will be used for the ` pk ` value.
166
189
167
- ### Indexing
190
+ Sort keys will contain that value and will be prefixed by the sort key prefix.
191
+
192
+ ###### Indexing
168
193
169
194
Cryptanomo supports indexing of encrypted fields for searching.
170
- Exact, prefix and compound match types are all supported.
195
+ Exact, prefix and compound match types are currently supported.
171
196
To index a field, use the ` query ` attribute:
172
197
173
198
``` rust
174
- use cryptonamo :: Cryptonamo ;
199
+ use cryptonamo :: Encryptable ;
175
200
176
- #[derive(Cryptonamo )]
201
+ #[derive(Debug , Encryptable )]
177
202
struct User {
178
203
#[cryptonamo(query = " exact" )]
179
204
#[partition_key]
180
205
email : String ,
181
-
206
+
182
207
#[cryptonamo(query = " prefix" )]
183
208
name : String ,
184
209
}
185
210
```
186
211
187
212
You can also specify a compound index by using the ` compound ` attribute.
188
- All indexes with the same compound name are combined into a single index.
213
+ Indexes with the same name will be combined into the one index.
214
+
215
+ Compound index names must be a combination of field names separated by a #.
216
+ Fields mentioned in the compound index name that aren't correctly annottated will result in a
217
+ compilation error.
189
218
190
219
``` rust
191
- use cryptonamo :: Cryptonamo ;
220
+ use cryptonamo :: Encryptable ;
192
221
193
- #[derive(Cryptonamo )]
222
+ #[derive(Debug , Encryptable )]
194
223
struct User {
195
224
#[cryptonamo(query = " exact" , compound = " email#name" )]
196
225
#[partition_key]
197
226
email : String ,
198
-
227
+
199
228
#[cryptonamo(query = " prefix" , compound = " email#name" )]
200
229
name : String ,
201
230
}
202
231
```
203
232
204
- ** NOTE:** Compound indexes defined using the ` compound ` attribute are not currently working.
205
- Check out [ SearchableRecord] for more information on how to implement compound indexes.
233
+ It's also possible to add more than one query attribute to support querying records in multiple
234
+ different ways.
235
+
206
236
207
- ### Storing and Retrieving Records
237
+ ``` rust
238
+ use cryptonamo :: Encryptable ;
239
+
240
+ #[derive(Debug , Encryptable )]
241
+ struct User {
242
+ #[cryptonamo(query = " exact" )]
243
+ #[cryptonamo(query = " exact" , compound = " email#name" )]
244
+ #[partition_key]
245
+ email : String ,
246
+
247
+ #[cryptonamo(query = " prefix" )]
248
+ #[cryptonamo(query = " exact" )]
249
+ #[cryptonamo(query = " prefix" , compound = " email#name" )]
250
+ name : String ,
251
+ }
252
+ ```
253
+ It's important to note that the more annotations that are added to a field the more index terms that will be generated. Adding too many attributes could result in a
254
+ proliferation of terms and data.
255
+
256
+ ###### Storing and Retrieving Records
208
257
209
258
Interacting with a table in DynamoDB is done via the [ EncryptedTable] struct.
210
259
@@ -220,48 +269,47 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
220
269
221
270
let client = aws_sdk_dynamodb :: Client :: new (& config );
222
271
let table = EncryptedTable :: init (client , " users" ). await ? ;
272
+
273
+ Ok (())
223
274
}
224
275
```
225
276
226
277
All operations on the table are ` async ` and so you will need a runtime to execute them.
227
278
In the above example, we connect to a DynamoDB running in a local container and initialize an ` EncryptedTable ` struct
228
279
for the "users" table.
229
280
230
- #### Putting Records
281
+ ####### Putting Records
231
282
232
283
To store a record in the table, use the [ ` EncryptedTable::put ` ] method:
233
284
234
285
``` rust
235
- #
236
286
let user = User :: new (" dan@coderdan" , " Dan Draper" );
237
- table . put (& user ). await ? ;
287
+ table . put (user ). await ? ;
238
288
```
239
289
240
290
To get a record, use the [ ` EncryptedTable::get ` ] method:
241
291
242
292
``` rust
243
- #
293
+
244
294
let user : Option <User > = table . get (" dan@coderdan.co" ). await ? ;
245
295
```
246
296
247
297
The ` get ` method will return ` None ` if the record does not exist.
248
298
It uses type information to decrypt the record and return it as a struct.
249
299
250
- #### Deleting Records
300
+ ####### Deleting Records
251
301
252
302
To delete a record, use the [ ` EncryptedTable::delete ` ] method:
253
303
254
304
``` rust
255
- #
256
305
table . delete :: <User >(" jane@smith.org" ). await ? ;
257
306
```
258
307
259
- #### Querying Records
308
+ ####### Querying Records
260
309
261
310
To query records, use the [ ` EncryptedTable::query ` ] method which returns a builder:
262
311
263
312
``` rust
264
- #
265
313
let results : Vec <User > = table
266
314
. query ()
267
315
. starts_with (" name" , " Dan" )
@@ -272,7 +320,6 @@ let results: Vec<User> = table
272
320
If you have a compound index defined, Cryptonamo will automatically use it when querying.
273
321
274
322
``` rust
275
- #
276
323
let results : Vec <User > = table
277
324
. query ()
278
325
. eq (" email" , " dan@coderdan" )
@@ -281,17 +328,20 @@ let results: Vec<User> = table
281
328
. await ? ;
282
329
```
283
330
284
- ### Table Verticalization
331
+ Note: if you don't have the correct indexes defined this query builder will return a runtime
332
+ error.
333
+
334
+ ###### Table Verticalization
285
335
286
336
Cryptonamo uses a technique called "verticalization" which is a popular approach to storing data in DynamoDB.
287
337
In practice, this means you can store multiple types in the same table.
288
338
289
339
For example, you might want to store related records to ` User ` such as ` License ` .
290
340
291
341
``` rust
292
- use cryptonamo :: Cryptonamo ;
342
+ use cryptonamo :: { Searchable , Encryptable , Decryptable } ;
293
343
294
- #[derive(Cryptonamo )]
344
+ #[derive(Debug , Searchable , Encryptable , Decryptable )]
295
345
struct License {
296
346
#[cryptonamo(query = " exact" )]
297
347
#[partition_key]
@@ -305,18 +355,19 @@ struct License {
305
355
}
306
356
```
307
357
308
- #### Data Views
358
+ ####### Data Views
309
359
310
360
In some cases, these types might simply be a different representation of the same data based on query requirements.
311
361
For example, you might want to query users by name using a prefix (say for using a "type ahead") but only return the name.
312
362
313
363
``` rust
314
- #[derive(Cryptonamo )]
364
+
365
+ #[derive(Debug , Searchable , Encryptable , Decryptable )]
315
366
pub struct UserView {
316
367
#[cryptonamo(skip)]
317
368
#[partition_key]
318
369
email : String ,
319
-
370
+
320
371
#[cryptonamo(query = " prefix" )]
321
372
name : String ,
322
373
}
@@ -326,7 +377,7 @@ To use the view, you can first `put` and then `query` the value.
326
377
327
378
``` rust
328
379
let user = UserView :: new (" dan@coderdan" , " Dan Draper" );
329
- table . put (& user ). await ? ;
380
+ table . put (user ). await ? ;
330
381
let results : Vec <UserView > = table
331
382
. query ()
332
383
. starts_with (" name" , " Dan" )
@@ -336,13 +387,13 @@ let results: Vec<UserView> = table
336
387
337
388
So long as the indexes are equivalent, you can mix and match types.
338
389
339
- ### Internals
390
+ ###### Internals
340
391
341
- #### Table Schema
392
+ ####### Table Schema
342
393
343
394
Tables created by Cryptonamo have the following schema:
344
395
345
- ``` rust
396
+ ``` txt
346
397
PK | SK | term | name | email ....
347
398
---------------------------------------------------------------------------
348
399
HMAC(123) | user | | Enc(name) | Enc(email)
@@ -358,7 +409,7 @@ HMAC(123) | user#name#4 | STE("Mike R") |
358
409
And all other attributes are dependent on the type.
359
410
They may be encrypted or otherwise.
360
411
361
- #### Source Encryption
412
+ ####### Source Encryption
362
413
363
414
Cryptonamo uses the CipherStash SDK to encrypt and decrypt data.
364
415
Values are encypted using a unique key for each record using AES-GCM-SIV with 256-bit keys.
@@ -368,9 +419,8 @@ ZeroKMS's root keys are encrypted using AWS KMS and stored in DynamoDB (separate
368
419
369
420
When self-hosting ZeroKMS, we recommend running it in different account to your main application workloads.
370
421
371
- ### Issues and TODO
422
+ ###### Issues and TODO
372
423
373
- - [ ] Support for plaintext types is currently not implemented
374
- - [ ] Using the derive macros for compound macros is not working correctly (you can implement the traits directly)
375
424
- [ ] Sort keys are not currently hashed (and should be)
376
425
426
+ <!-- cargo-rdme end -->
0 commit comments