Skip to content

Commit e7a3c21

Browse files
committed
refactor(keystore): rework item-level encryption traits
The existing `trait EntityEncryptionExt` was confusing: it depended on mutating the internal state, and never calling the appropriate methods too many or too few times. It wasn't obvious when or whether it was ever appropriate for an implementing item to override one of the default trait methods. In fairness, it's a complicated problem. This commit contains my approach, which breaks it up substantially. Entities no longer implement `Serialize` or `Deserialize`, and in fact must not implement either of those. (If it were possible, I'd have added negative trait bounds to that effect.) Doing this with separate types gives us some static type safety, which is always nice! Instead, entities which want to serialize must implement `Encrypting`, which requires them to copy all non-sensitive data and encrypt all sensitive data into an associated type. This associated type implements `Serialize`. Likewise, entities which want to deserialize must implement `Decrypting`, which requires them to copy all non-sensitive data and decrypt all sensitive data from an associated type. This associated type implements `Deserialize`. The benefit is that required methods have no defaults, and it is impossible to improperly override default methods. There are now helper traits `EncryptData` and `DecryptData`, which are automatically implemented for all entities, and handle all the details of the symmetrical encryption and decryption.
1 parent 3d649b3 commit e7a3c21

File tree

10 files changed

+316
-96
lines changed

10 files changed

+316
-96
lines changed

keystore/src/lib.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@ pub(crate) mod proteus;
1111
mod traits;
1212
pub mod transaction;
1313

14-
pub use self::connection::{ConnectionType, Database, DatabaseKey};
14+
#[cfg(not(target_family = "wasm"))]
15+
use sha2::{Digest, Sha256};
16+
1517
#[cfg(feature = "dummy-entity")]
1618
pub use self::entities::{DummyStoreValue, DummyValue};
17-
pub use self::error::*;
18-
#[cfg(target_family = "wasm")]
19-
pub use self::traits::EntityEncryptionExt;
20-
pub use self::traits::{
21-
Entity, EntityBase, EntityGetBorrowed, EntityTransactionDeleteBorrowed, EntityTransactionExt, FetchFromDatabase,
22-
KeyType, UniqueEntity, UniqueEntityExt,
19+
pub use self::{
20+
connection::{ConnectionType, Database, DatabaseKey},
21+
error::*,
22+
traits::{
23+
DecryptData, Decryptable, Decrypting, EncryptData, Encrypting, Entity, EntityBase, EntityGetBorrowed,
24+
EntityTransactionDeleteBorrowed, EntityTransactionExt, FetchFromDatabase, KeyType, UniqueEntity,
25+
UniqueEntityExt,
26+
},
2327
};
2428

25-
#[cfg(not(target_family = "wasm"))]
26-
use sha2::{Digest, Sha256};
27-
2829
/// Used to calculate ID hashes for some MlsEntities' SQLite tables (not used on wasm).
2930
/// We only use sha256 on platforms where we use SQLite.
3031
/// On wasm, we use IndexedDB, a key-value store, via the idb crate.

keystore/src/traits/entity.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::borrow::Borrow;
22

33
use async_trait::async_trait;
4-
use serde::{Serialize, de::DeserializeOwned};
54

65
use crate::{CryptoKeystoreResult, EntityBase, KeyType};
76

@@ -10,7 +9,7 @@ use crate::{CryptoKeystoreResult, EntityBase, KeyType};
109
/// It has a primary key, which uniquely identifies it.
1110
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
1211
#[cfg_attr(not(target_family = "wasm"), async_trait)]
13-
pub trait Entity: EntityBase + Serialize + DeserializeOwned {
12+
pub trait Entity: EntityBase {
1413
/// Each distinct [`PrimaryKey`] uniquely identifies either 0 or 1 instance.
1514
///
1615
/// This constraint should be enforced at the DB level.

keystore/src/traits/entity_encryption_ext.rs

Lines changed: 0 additions & 80 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
use crate::{CryptoKeystoreResult, Entity, KeyType as _};
2+
3+
pub(super) const AES_GCM_256_NONCE_SIZE: usize = 12;
4+
5+
#[derive(core_crypto_macros::Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
6+
pub(super) struct Aad {
7+
type_name: Vec<u8>,
8+
id: Vec<u8>,
9+
}
10+
11+
impl<E> From<&'_ E> for Aad
12+
where
13+
E: Entity,
14+
{
15+
fn from(value: &E) -> Self {
16+
let type_name = E::COLLECTION_NAME.as_bytes().to_vec();
17+
let id = value.primary_key().bytes().into_owned();
18+
Self { type_name, id }
19+
}
20+
}
21+
22+
impl Aad {
23+
pub(super) fn serialize(&self) -> CryptoKeystoreResult<Vec<u8>> {
24+
serde_json::to_vec(self).map_err(Into::into)
25+
}
26+
27+
pub(super) fn from_primary_key<E: Entity>(primary_key: &E::PrimaryKey) -> Self {
28+
let type_name = E::COLLECTION_NAME.as_bytes().to_vec();
29+
let id = primary_key.bytes().into_owned();
30+
Self { type_name, id }
31+
}
32+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use super::aad::{AES_GCM_256_NONCE_SIZE, Aad};
2+
use crate::{CryptoKeystoreError, CryptoKeystoreResult, Entity, EntityTransactionDeleteBorrowed, KeyType as _};
3+
4+
fn decrypt_with_nonce_and_aad(
5+
cipher: &aes_gcm::Aes256Gcm,
6+
msg: &[u8],
7+
nonce: &[u8],
8+
aad: &[u8],
9+
) -> CryptoKeystoreResult<Vec<u8>> {
10+
use aes_gcm::aead::Aead as _;
11+
12+
let nonce = aes_gcm::Nonce::from_slice(nonce);
13+
let payload = aes_gcm::aead::Payload { msg, aad };
14+
15+
let cleartext = cipher
16+
.decrypt(nonce, payload)
17+
.map_err(|_| CryptoKeystoreError::AesGcmError)?;
18+
19+
Ok(cleartext)
20+
}
21+
22+
/// This trait is intended to provide a convenient way to decrypt data.
23+
///
24+
/// There is a blanket implementation covering all [`Entity`]s.
25+
pub trait DecryptData: Entity {
26+
/// Decrypt some data, symmetrically to the process [`encrypt_data`][super::EncryptData::encrypt_data] uses.
27+
fn decrypt_data(
28+
cipher: &aes_gcm::Aes256Gcm,
29+
primary_key: &<Self as Entity>::PrimaryKey,
30+
data: &[u8],
31+
) -> CryptoKeystoreResult<Vec<u8>>;
32+
}
33+
34+
impl<E: Entity> DecryptData for E {
35+
fn decrypt_data(
36+
cipher: &aes_gcm::Aes256Gcm,
37+
primary_key: &E::PrimaryKey,
38+
data: &[u8],
39+
) -> CryptoKeystoreResult<Vec<u8>> {
40+
let aad = Aad::from_primary_key(primary_key).serialize()?;
41+
let (nonce, msg) = data
42+
.split_at_checked(AES_GCM_256_NONCE_SIZE)
43+
.ok_or(CryptoKeystoreError::AesGcmError)?;
44+
decrypt_with_nonce_and_aad(cipher, msg, &nonce, &aad)
45+
}
46+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use serde::Deserialize;
2+
3+
use crate::{CryptoKeystoreResult, Entity};
4+
5+
/// This trait restores to a plaintext form of this struct, where all sensitive fields have been
6+
/// decrypted.
7+
///
8+
/// This is quite likely to be handled automatically by a macro, depending on how annoying it is to implement everywhere.
9+
///
10+
/// ## Example
11+
///
12+
/// ```rust,ignore
13+
/// // Foo is an Entity
14+
/// struct Foo {
15+
/// id: Vec<u8>,
16+
/// sensitive_data: Vec<u8>, // sensitive!
17+
/// }
18+
///
19+
/// #[derive(serde::Serialize)]
20+
/// struct EncryptedFoo<'de> {
21+
/// id: Vec<u8>,
22+
/// sensitive_data: &'de [u8],
23+
/// }
24+
///
25+
/// impl<'de> Decrypting<'de> for EncryptedFoo<'de> {
26+
/// type DecryptedForm = Foo;
27+
///
28+
/// fn decrypt(self, cipher: &aes_gcm::Aes256Gcm) -> CryptoKeystoreResult<Foo> {
29+
/// let id = self.id;
30+
/// let sensitive_data = E::decrypt_data(cipher, &id, self.sensitive_data)?;
31+
/// Ok(Foo {
32+
/// id,
33+
/// sensitive_data,
34+
/// })
35+
/// }
36+
/// }
37+
/// ```
38+
///
39+
/// This can then be used like:
40+
///
41+
/// ```rust,ignore
42+
/// let foo = serde_json::from_str::<EncryptedFoo>(json)?.decrypt(cipher)?;
43+
/// ```
44+
pub trait Decrypting<'de>: 'de + Deserialize<'de> {
45+
type DecryptedForm: Entity;
46+
47+
fn decrypt(self, cipher: &aes_gcm::Aes256Gcm) -> CryptoKeystoreResult<Self::DecryptedForm>;
48+
}
49+
50+
/// Helper trait for restoring from an encrypted form of this struct.
51+
///
52+
/// This is mainly useful so that the encrypted form does not need to be named, or even nameable.
53+
///
54+
/// This is quite likely to be handled automatically by a macro, depending on how annoying it is to implement everywhere.
55+
///
56+
/// ## Example
57+
///
58+
/// Extending the example from [`Decrypting`]:
59+
///
60+
/// ```rust,ignore
61+
/// // Foo is an Entity
62+
/// struct Foo { ... }
63+
///
64+
/// #[derive(serde::Serialize)]
65+
/// struct EncryptedFoo<'de> { ... }
66+
///
67+
/// impl<'de> Decrypting<'de> for EncryptedFoo<'de> {
68+
/// type DecryptedForm = Foo;
69+
/// fn decrypt(self, cipher: &aes_gcm::Aes256Gcm) -> CryptoKeystoreResult<Foo> { ... }
70+
/// }
71+
///
72+
/// impl<'de> Decryptable<'de> for Foo {
73+
/// type DecryptableFrom = EncryptedFoo<'de>;
74+
/// }
75+
/// ```
76+
///
77+
/// `EncryptedFoo` now no longer needs to appear in external code:
78+
///
79+
/// ```rust,ignore
80+
/// let foo = serde_json::from_str::<Foo::DecryptableFrom>(json)?.decrypt(cipher)?;
81+
/// ```
82+
pub trait Decryptable<'de>: Entity {
83+
type DecryptableFrom: 'de + Decrypting<'de>;
84+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use crate::{CryptoKeystoreError, CryptoKeystoreResult, Entity};
2+
3+
use super::aad::{AES_GCM_256_NONCE_SIZE, Aad};
4+
5+
// About WASM Encryption:
6+
// The store key (i.e. passphrase) is hashed using SHA256 to obtain 32 bytes
7+
// The AES256-GCM cipher is then initialized and is used to encrypt individual values
8+
// Entities shall decide which fields need to be encrypted
9+
// Internal layout:
10+
// - Cleartext: [u8] bytes
11+
// - Ciphertext: [12 bytes of nonce..., ...encrypted data]
12+
fn encrypt_with_nonce_and_aad(
13+
cipher: &aes_gcm::Aes256Gcm,
14+
msg: &[u8],
15+
nonce: &[u8],
16+
aad: &[u8],
17+
) -> CryptoKeystoreResult<Vec<u8>> {
18+
use aes_gcm::aead::Aead as _;
19+
20+
let nonce = aes_gcm::Nonce::from_slice(nonce);
21+
let payload = aes_gcm::aead::Payload { msg, aad };
22+
23+
let mut encrypted = cipher
24+
.encrypt(nonce, payload)
25+
.map_err(|_| CryptoKeystoreError::AesGcmError)?;
26+
let mut message = Vec::with_capacity(nonce.len() + encrypted.len());
27+
message.extend_from_slice(nonce);
28+
message.append(&mut encrypted);
29+
Ok(message)
30+
}
31+
32+
/// This trait is intended to provide a convenient way to encrypt data.
33+
///
34+
/// The encryption process embeds both a nonce and an AAD: an identity comprising
35+
/// both the entity's type and a unique identifier.
36+
///
37+
/// There is a blanket implementation covering all [`Entity`]s.
38+
pub trait EncryptData {
39+
/// Encrypt some data, using a random nonce and an AAD.
40+
fn encrypt_data(&self, cipher: &aes_gcm::Aes256Gcm, data: &[u8]) -> CryptoKeystoreResult<Vec<u8>>;
41+
}
42+
43+
impl<E: Entity> EncryptData for E {
44+
fn encrypt_data(&self, cipher: &aes_gcm::Aes256Gcm, data: &[u8]) -> CryptoKeystoreResult<Vec<u8>> {
45+
let aad = Aad::from(self).serialize()?;
46+
let nonce_bytes: [u8; AES_GCM_256_NONCE_SIZE] = rand::random();
47+
encrypt_with_nonce_and_aad(cipher, data, &nonce_bytes, &aad)
48+
}
49+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use serde::Serialize;
2+
3+
use crate::CryptoKeystoreResult;
4+
5+
/// This trait produces an encrypted form of this struct, where all sensitive fields have been
6+
/// encrypted using the [`EncryptData`][super::EncryptData] helper.
7+
///
8+
/// This is quite likely to be handled automatically by a macro, depending on how annoying it is to implement everywhere.
9+
///
10+
/// ## Example
11+
///
12+
/// ```rust,ignore
13+
/// // Foo is an Entity
14+
/// struct Foo {
15+
/// id: Vec<u8>,
16+
/// sensitive_data: Vec<u8>, // sensitive!
17+
/// }
18+
///
19+
/// #[derive(serde::Serialize)]
20+
/// struct EncryptedFoo<'a> {
21+
/// id: &'a Vec<u8>,
22+
/// sensitive_data: Vec<u8>,
23+
/// }
24+
///
25+
/// impl<'a> Encrypting<'a> for Foo {
26+
/// type EncryptedForm: EncryptedFoo<'a>;
27+
///
28+
/// fn encrypt(&'a self, cipher: &aes_gcm::Aes256Gcm) -> CryptoKeystoreResult<EncryptedFoo<'a>> {
29+
/// Ok(EncryptedFoo {
30+
/// id: &self.id,
31+
/// sensitive_data: self.encrypt_data(cipher, &self.sensitive_data)?,
32+
/// })
33+
/// }
34+
/// }
35+
/// ```
36+
///
37+
/// This can then be used like:
38+
///
39+
/// ```rust,ignore
40+
/// let json = serde_json::to_string(&foo.encrypt(cipher)?)?;
41+
/// ```
42+
pub trait Encrypting<'a> {
43+
/// This type must be serializable, but can depend on the lifetime of `self` to reduce copying.
44+
type EncryptedForm: 'a + Serialize;
45+
46+
/// Make an instance of the encrypted form of this struct, for which all sensitive fields have been encrypted.
47+
fn encrypt(&'a self, cipher: &aes_gcm::Aes256Gcm) -> CryptoKeystoreResult<Self::EncryptedForm>;
48+
}

0 commit comments

Comments
 (0)