Skip to content

Commit 985fb03

Browse files
author
Bennett Hardwick
committed
Update README with cargo-rdme
1 parent 279fbed commit 985fb03

File tree

1 file changed

+118
-68
lines changed

1 file changed

+118
-68
lines changed

README.md

Lines changed: 118 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,21 @@ Upload it to ZeroKMS using the following command:
5151

5252
---
5353

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
5562

5663
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.
5869

5970
You can use the the `aws` CLI to create a table with an appropriate schema as follows:
6071

@@ -69,44 +80,45 @@ aws dynamodb create-table \
6980
AttributeName=pk,KeyType=HASH \
7081
AttributeName=sk,KeyType=RANGE \
7182
--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}"
7384
```
7485

7586
See below for more information on schema design for Cryptonamo tables.
7687

77-
#### Annotating a Cryptanomo Type
88+
####### Annotating a Cryptanomo Type
7889

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.
8092

8193
```rust
82-
use cryptonamo::{Encryptable, Decryptable, Searchable};
94+
use cryptonamo::{Searchable, Decryptable, Encryptable};
8395

84-
#[derive(Debug, Encryptable, Decryptable, Searchable)]
96+
#[derive(Debug, Searchable, Decryptable, Encryptable)]
8597
struct User {
8698
name: String,
87-
8899
#[partition_key]
89100
email: String,
90101
}
91102
```
92103

93-
This example implements the traits:
104+
These derive macros will generate implementations for the following traits of the same name:
94105

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
97108
* `Searchable` - a trait that allows you to search for records in DynamoDB
98109

99110
The above example is the minimum required to use Cryptonamo however you can expand capabilities via several macros.
100111

101-
#### Controlling Encryption
112+
####### Controlling Encryption
102113

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:
105117

106118
```rust
107-
use cryptonamo::Cryptonamo;
119+
use cryptonamo::{Searchable, Decryptable, Encryptable};
108120

109-
#[derive(Cryptonamo)]
121+
#[derive(Debug, Searchable, Decryptable, Encryptable)]
110122
struct User {
111123
#[partition_key]
112124
email: String,
@@ -117,22 +129,34 @@ struct User {
117129
}
118130
```
119131

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)]`.
121133

122134
```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,
127145
}
128146
```
129147

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:
131154

132155
```rust
133-
use cryptonamo::Cryptonamo;
156+
use cryptonamo::Encryptable;
134157

135-
#[derive(Cryptonamo)]
158+
#[derive(Debug, Encryptable)]
159+
#[cryptonamo(sort_key_prefix = "user")]
136160
struct User {
137161
#[partition_key]
138162
email: String,
@@ -143,68 +167,93 @@ struct User {
143167
}
144168
```
145169

146-
#### Sort keys
170+
######## Dynamic Sort keys
147171

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.
150174

151175
```rust
152-
use cryptonamo::Cryptonamo;
176+
use cryptonamo::Encryptable;
153177

154-
#[derive(Cryptonamo)]
155-
#[cryptonamo(partition_key = "email")]
156-
#[cryptonamo(sort_key_prefix = "user")]
178+
#[derive(Debug, Encryptable)]
157179
struct User {
180+
#[partition_key]
181+
email: String,
182+
#[sort_key]
158183
name: String,
159184

160185
#[cryptonamo(skip)]
161186
not_required: String,
162187
}
163188
```
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.
166189

167-
### Indexing
190+
Sort keys will contain that value and will be prefixed by the sort key prefix.
191+
192+
###### Indexing
168193

169194
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.
171196
To index a field, use the `query` attribute:
172197

173198
```rust
174-
use cryptonamo::Cryptonamo;
199+
use cryptonamo::Encryptable;
175200

176-
#[derive(Cryptonamo)]
201+
#[derive(Debug, Encryptable)]
177202
struct User {
178203
#[cryptonamo(query = "exact")]
179204
#[partition_key]
180205
email: String,
181-
206+
182207
#[cryptonamo(query = "prefix")]
183208
name: String,
184209
}
185210
```
186211

187212
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.
189218

190219
```rust
191-
use cryptonamo::Cryptonamo;
220+
use cryptonamo::Encryptable;
192221

193-
#[derive(Cryptonamo)]
222+
#[derive(Debug, Encryptable)]
194223
struct User {
195224
#[cryptonamo(query = "exact", compound = "email#name")]
196225
#[partition_key]
197226
email: String,
198-
227+
199228
#[cryptonamo(query = "prefix", compound = "email#name")]
200229
name: String,
201230
}
202231
```
203232

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+
206236

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
208257

209258
Interacting with a table in DynamoDB is done via the [EncryptedTable] struct.
210259

@@ -220,48 +269,47 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
220269

221270
let client = aws_sdk_dynamodb::Client::new(&config);
222271
let table = EncryptedTable::init(client, "users").await?;
272+
273+
Ok(())
223274
}
224275
```
225276

226277
All operations on the table are `async` and so you will need a runtime to execute them.
227278
In the above example, we connect to a DynamoDB running in a local container and initialize an `EncryptedTable` struct
228279
for the "users" table.
229280

230-
#### Putting Records
281+
####### Putting Records
231282

232283
To store a record in the table, use the [`EncryptedTable::put`] method:
233284

234285
```rust
235-
#
236286
let user = User::new("dan@coderdan", "Dan Draper");
237-
table.put(&user).await?;
287+
table.put(user).await?;
238288
```
239289

240290
To get a record, use the [`EncryptedTable::get`] method:
241291

242292
```rust
243-
#
293+
244294
let user: Option<User> = table.get("dan@coderdan.co").await?;
245295
```
246296

247297
The `get` method will return `None` if the record does not exist.
248298
It uses type information to decrypt the record and return it as a struct.
249299

250-
#### Deleting Records
300+
####### Deleting Records
251301

252302
To delete a record, use the [`EncryptedTable::delete`] method:
253303

254304
```rust
255-
#
256305
table.delete::<User>("jane@smith.org").await?;
257306
```
258307

259-
#### Querying Records
308+
####### Querying Records
260309

261310
To query records, use the [`EncryptedTable::query`] method which returns a builder:
262311

263312
```rust
264-
#
265313
let results: Vec<User> = table
266314
.query()
267315
.starts_with("name", "Dan")
@@ -272,7 +320,6 @@ let results: Vec<User> = table
272320
If you have a compound index defined, Cryptonamo will automatically use it when querying.
273321

274322
```rust
275-
#
276323
let results: Vec<User> = table
277324
.query()
278325
.eq("email", "dan@coderdan")
@@ -281,17 +328,20 @@ let results: Vec<User> = table
281328
.await?;
282329
```
283330

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
285335

286336
Cryptonamo uses a technique called "verticalization" which is a popular approach to storing data in DynamoDB.
287337
In practice, this means you can store multiple types in the same table.
288338

289339
For example, you might want to store related records to `User` such as `License`.
290340

291341
```rust
292-
use cryptonamo::Cryptonamo;
342+
use cryptonamo::{ Searchable, Encryptable, Decryptable };
293343

294-
#[derive(Cryptonamo)]
344+
#[derive(Debug, Searchable, Encryptable, Decryptable)]
295345
struct License {
296346
#[cryptonamo(query = "exact")]
297347
#[partition_key]
@@ -305,18 +355,19 @@ struct License {
305355
}
306356
```
307357

308-
#### Data Views
358+
####### Data Views
309359

310360
In some cases, these types might simply be a different representation of the same data based on query requirements.
311361
For example, you might want to query users by name using a prefix (say for using a "type ahead") but only return the name.
312362

313363
```rust
314-
#[derive(Cryptonamo)]
364+
365+
#[derive(Debug, Searchable, Encryptable, Decryptable)]
315366
pub struct UserView {
316367
#[cryptonamo(skip)]
317368
#[partition_key]
318369
email: String,
319-
370+
320371
#[cryptonamo(query = "prefix")]
321372
name: String,
322373
}
@@ -326,7 +377,7 @@ To use the view, you can first `put` and then `query` the value.
326377

327378
```rust
328379
let user = UserView::new("dan@coderdan", "Dan Draper");
329-
table.put(&user).await?;
380+
table.put(user).await?;
330381
let results: Vec<UserView> = table
331382
.query()
332383
.starts_with("name", "Dan")
@@ -336,13 +387,13 @@ let results: Vec<UserView> = table
336387

337388
So long as the indexes are equivalent, you can mix and match types.
338389

339-
### Internals
390+
###### Internals
340391

341-
#### Table Schema
392+
####### Table Schema
342393

343394
Tables created by Cryptonamo have the following schema:
344395

345-
```rust
396+
```txt
346397
PK | SK | term | name | email ....
347398
---------------------------------------------------------------------------
348399
HMAC(123) | user | | Enc(name) | Enc(email)
@@ -358,7 +409,7 @@ HMAC(123) | user#name#4 | STE("Mike R") |
358409
And all other attributes are dependent on the type.
359410
They may be encrypted or otherwise.
360411

361-
#### Source Encryption
412+
####### Source Encryption
362413

363414
Cryptonamo uses the CipherStash SDK to encrypt and decrypt data.
364415
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
368419

369420
When self-hosting ZeroKMS, we recommend running it in different account to your main application workloads.
370421

371-
### Issues and TODO
422+
###### Issues and TODO
372423

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)
375424
- [ ] Sort keys are not currently hashed (and should be)
376425

426+
<!-- cargo-rdme end -->

0 commit comments

Comments
 (0)