Skip to content

Commit 479a7db

Browse files
authored
Merge pull request #10 from cipherstash/docs
Docs
2 parents eb75a9b + 5546d51 commit 479a7db

File tree

3 files changed

+728
-12
lines changed

3 files changed

+728
-12
lines changed

README.md

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
# cryptonamo
2+
3+
### Cryptonamo: Encrypted Tables for DynamoDB
4+
5+
Based on the CipherStash SDK and ZeroKMS key service, Cryptonamo provides a simple interface for
6+
storing and retrieving encrypted data in DynamoDB.
7+
8+
### Usage
9+
10+
To use Cryptonamo, you must first create a table in DynamoDB.
11+
The table must have a primary key and sort key, both of type String.
12+
13+
You can use the the `aws` CLI to create a table with an appropriate schema as follows:
14+
15+
```bash
16+
aws dynamodb create-table \
17+
--table-name users \
18+
--attribute-definitions \
19+
AttributeName=pk,AttributeType=S \
20+
AttributeName=sk,AttributeType=S \
21+
AttributeName=term,AttributeType=S \
22+
--key-schema \
23+
AttributeName=pk,KeyType=HASH \
24+
AttributeName=sk,KeyType=RANGE \
25+
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
26+
--global-secondary-indexes "IndexName=TermIndex,KeySchema=[{AttributeName=term,KeyType=HASH},{AttributeName=pk,KeyType=RANGE}],Projection={ProjectionType=ALL},ProvisionedThroughput={ReadCapacityUnits=5,WriteCapacityUnits=5}"
27+
```
28+
29+
See below for more information on schema design for Cryptonamo tables.
30+
31+
#### Annotating a Cryptanomo Type
32+
33+
To use Cryptonamo, you must first annotate a struct with the `Cryptonamo` derive macro.
34+
35+
```rust
36+
use cryptonamo::Cryptonamo;
37+
38+
#[derive(Cryptonamo)]
39+
#[cryptonamo(partition_key = "email")]
40+
struct User {
41+
name: String,
42+
email: String,
43+
}
44+
```
45+
46+
The `Cryptonamo` derive macro will generate implementations of the following traits:
47+
48+
* `Cryptonamo` - a top-level trait that sets up the table name and partition key
49+
* `DecryptedRecord` - a trait that allows you to decrypt a record from DynamoDB
50+
* `EncryptedRecord` - a trait that allows you to encrypt a record for storage in DynamoDB
51+
* `SearchableRecord` - a trait that allows you to search for records in DynamoDB
52+
53+
The above example is the minimum required to use Cryptonamo however you can expand capabilities via several macros.
54+
55+
#### Controlling Encryption
56+
57+
By default, all fields on a `Cryptanomo` type are encrypted and stored in the index.
58+
To store a field as a plaintext, use the `plaintext` attribute:
59+
60+
```rust
61+
use cryptonamo::Cryptonamo;
62+
63+
#[derive(Cryptonamo)]
64+
#[cryptonamo(partition_key = "email")]
65+
struct User {
66+
email: String,
67+
name: String,
68+
69+
#[cryptonamo(plaintext)]
70+
not_sensitive: String,
71+
}
72+
```
73+
74+
Most basic rust types will work automatically but you can implement a conversion trait for [Plaintext] to support custom types.
75+
76+
```rust
77+
impl From<MyType> for Plaintext {
78+
fn from(t: MyType) -> Self {
79+
t.as_bytes().into()
80+
}
81+
}
82+
```
83+
84+
If you don't want a field stored in the the database at all, you can annotate the field with `#[cryptonamo(skip)]`.
85+
86+
```rust
87+
use cryptonamo::Cryptonamo;
88+
89+
#[derive(Cryptonamo)]
90+
#[cryptonamo(partition_key = "email")]
91+
struct User {
92+
email: String,
93+
name: String,
94+
95+
#[cryptonamo(skip)]
96+
not_required: String,
97+
}
98+
```
99+
100+
#### Sort keys
101+
102+
Cryptanomo requires every record to have a sort key and it derives it automatically based on the name of the struct.
103+
However, if you want to specify your own, you can use the `sort_key_prefix` attribute:
104+
105+
```rust
106+
use cryptonamo::Cryptonamo;
107+
108+
#[derive(Cryptonamo)]
109+
#[cryptonamo(partition_key = "email")]
110+
#[cryptonamo(sort_key_prefix = "user")]
111+
struct User {
112+
name: String,
113+
114+
#[cryptonamo(skip)]
115+
not_required: String,
116+
}
117+
```
118+
Note that you can `skip` the partition key as well.
119+
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.
120+
121+
### Indexing
122+
123+
Cryptanomo supports indexing of encrypted fields for searching.
124+
Exact, prefix and compound match types are all supported.
125+
To index a field, use the `query` attribute:
126+
127+
```rust
128+
use cryptonamo::Cryptonamo;
129+
130+
#[derive(Cryptonamo)]
131+
#[cryptonamo(partition_key = "email")]
132+
struct User {
133+
#[cryptonamo(query = "exact")]
134+
email: String,
135+
136+
#[cryptonamo(query = "prefix")]
137+
name: String,
138+
}
139+
```
140+
141+
You can also specify a compound index by using the `compound` attribute.
142+
All indexes with the same compound name are combined into a single index.
143+
144+
```rust
145+
use cryptonamo::Cryptonamo;
146+
147+
#[derive(Cryptonamo)]
148+
#[cryptonamo(partition_key = "email")]
149+
struct User {
150+
#[cryptonamo(query = "exact", compound = "email#name")]
151+
email: String,
152+
153+
#[cryptonamo(query = "prefix", compound = "email#name")]
154+
name: String,
155+
}
156+
```
157+
158+
**NOTE:** Compound indexes defined using the `compound` attribute are not currently working.
159+
Check out [SearchableRecord] for more information on how to implement compound indexes.
160+
161+
### Storing and Retrieving Records
162+
163+
Interacting with a table in DynamoDB is done via the [EncryptedTable] struct.
164+
165+
```rust
166+
use cryptonamo::{EncryptedTable, Key};
167+
168+
#[tokio::main]
169+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
170+
let config = aws_config::from_env()
171+
.endpoint_url("http://localhost:8000")
172+
.load()
173+
.await;
174+
175+
let client = aws_sdk_dynamodb::Client::new(&config);
176+
let table = EncryptedTable::init(client, "users").await?;
177+
}
178+
```
179+
180+
All operations on the table are `async` and so you will need a runtime to execute them.
181+
In the above example, we connect to a DynamoDB running in a local container and initialize an `EncryptedTable` struct
182+
for the "users" table.
183+
184+
#### Putting Records
185+
186+
To store a record in the table, use the [`EncryptedTable::put`] method:
187+
188+
```rust
189+
#
190+
let user = User::new("dan@coderdan", "Dan Draper");
191+
table.put(&user).await?;
192+
```
193+
194+
To get a record, use the [`EncryptedTable::get`] method:
195+
196+
```rust
197+
#
198+
let user: Option<User> = table.get("dan@coderdan.co").await?;
199+
```
200+
201+
The `get` method will return `None` if the record does not exist.
202+
It uses type information to decrypt the record and return it as a struct.
203+
204+
#### Deleting Records
205+
206+
To delete a record, use the [`EncryptedTable::delete`] method:
207+
208+
```rust
209+
#
210+
table.delete::<User>("jane@smith.org").await?;
211+
```
212+
213+
#### Querying Records
214+
215+
To query records, use the [`EncryptedTable::query`] method which returns a builder:
216+
217+
```rust
218+
#
219+
let results: Vec<User> = table
220+
.query()
221+
.starts_with("name", "Dan")
222+
.send()
223+
.await?;
224+
```
225+
226+
If you have a compound index defined, Cryptonamo will automatically use it when querying.
227+
228+
```rust
229+
#
230+
let results: Vec<User> = table
231+
.query()
232+
.eq("email", "dan@coderdan")
233+
.starts_with("name", "Dan")
234+
.send()
235+
.await?;
236+
```
237+
238+
### Table Verticalization
239+
240+
Cryptonamo uses a technique called "verticalization" which is a popular approach to storing data in DynamoDB.
241+
In practice, this means you can store multiple types in the same table.
242+
243+
For example, you might want to store related records to `User` such as `License`.
244+
245+
```rust
246+
use cryptonamo::Cryptonamo;
247+
248+
#[derive(Cryptonamo)]
249+
#[cryptonamo(partition_key = "user_email")]
250+
struct License {
251+
#[cryptonamo(query = "exact")]
252+
user_email: String,
253+
254+
#[cryptonamo(plaintext)]
255+
license_type: String,
256+
257+
#[cryptonamo(query = "exact")]
258+
license_number: String,
259+
}
260+
```
261+
262+
#### Data Views
263+
264+
In some cases, these types might simply be a different representation of the same data based on query requirements.
265+
For example, you might want to query users by name using a prefix (say for using a "type ahead") but only return the name.
266+
267+
```rust
268+
#[derive(Cryptonamo)]
269+
#[cryptonamo(partition_key = "email")]
270+
pub struct UserView {
271+
#[cryptonamo(skip)]
272+
email: String,
273+
274+
#[cryptonamo(query = "prefix")]
275+
name: String,
276+
}
277+
```
278+
279+
To use the view, you can first `put` and then `query` the value.
280+
281+
```rust
282+
let user = UserView::new("dan@coderdan", "Dan Draper");
283+
table.put(&user).await?;
284+
let results: Vec<UserView> = table
285+
.query()
286+
.starts_with("name", "Dan")
287+
.send()
288+
.await?;
289+
```
290+
291+
So long as the indexes are equivalent, you can mix and match types.
292+
293+
### Internals
294+
295+
#### Table Schema
296+
297+
Tables created by Cryptonamo have the following schema:
298+
299+
```rust
300+
PK | SK | term | name | email ....
301+
---------------------------------------------------------------------------
302+
HMAC(123) | user | | Enc(name) | Enc(email)
303+
HMAC(123) | user#email | STE("foo@example.net") |
304+
HMAC(123) | user#name#1 | STE("Mik") |
305+
HMAC(123) | user#name#2 | STE("Mike") |
306+
HMAC(123) | user#name#3 | STE("Mike ") |
307+
HMAC(123) | user#name#4 | STE("Mike R") |
308+
```
309+
310+
`PK` and `SK` are the partition and sort keys respectively.
311+
`term` is a global secondary index that is used for searching.
312+
And all other attributes are dependent on the type.
313+
They may be encrypted or otherwise.
314+
315+
#### Source Encryption
316+
317+
Cryptonamo uses the CipherStash SDK to encrypt and decrypt data.
318+
Values are encypted using a unique key for each record using AES-GCM-SIV with 256-bit keys.
319+
Key generation is performed using the ZeroKMS key service and bulk operations are supported making even large queries quite fast.
320+
321+
ZeroKMS's root keys are encrypted using AWS KMS and stored in DynamoDB (separate database to the data).
322+
323+
When self-hosting ZeroKMS, we recommend running it in different account to your main application workloads.
324+
325+
### Issues and TODO
326+
327+
- [ ] Support for plaintext types is currently not implemented
328+
- [ ] Using the derive macros for compound macros is not working correctly (you can implement the traits directly)
329+
- [ ] Sort keys are not currently hashed (and should be)
330+

dynamo.sh

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
#aws dynamodb create-table --endpoint-url http://localhost:8000 --table-name dict --attribute-definitions AttributeName=term_key,AttributeType=B --key-schema AttributeName=term_key,KeyType=HASH --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5
2-
3-
#aws dynamodb create-table \
4-
# --table-name postings \
5-
# --attribute-definitions \
6-
# AttributeName=term,AttributeType=B \
7-
# AttributeName=docid,AttributeType=S \
8-
# --key-schema AttributeName=term,KeyType=HASH \
9-
# --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
10-
# --global-secondary-indexes "IndexName=DocIDIndex,KeySchema=[{AttributeName=docid,KeyType=HASH}],Projection={ProjectionType=ALL},ProvisionedThroughput={ReadCapacityUnits=5,WriteCapacityUnits=5}" \
11-
# --endpoint-url http://localhost:8000
12-
131
aws dynamodb create-table \
142
--table-name users \
153
--attribute-definitions \

0 commit comments

Comments
 (0)