# Typelock
**Enforce security boundaries at the Type level**
`typelock` is a Rust framework for creating type-safe, encrypted and hashed data models. It uses a procedural macro to generate locked and unlocked versions of your data models to ensure sensitive data is only accessible through explicit, pre-defined, model conversions steps and to avoid type confusion.
## Features
Given a single input definition, `typelock` generates the following:
- An **unlocked** representation
- A **locked** representation
- Explicit `lock()` and `unlock()` transitions, based on the current model
## Example
```rust
#[derive(LockSchema, Clone)]
#[typelock(unlocked(name = UserUnlocked, derives(Debug)), locked(name = UserLocked))]
pub struct User {
pub id: i64,
#[secure(policy(encrypt))]
pub email: String,
#[secure(policy(hash), rename = "password_hash")]
pub password: String,
#[secure(policy(encrypt))]
pub user_data: UserData,
}
```
## Custom types
Secured fields must define how they are convered to and from byte. This is intentional, so that `typelock` doesn't force you into the usage of specific libraries.
```rust
#[derive(Debug, Clone)]
pub struct UserData {
first_name: String,
}
impl ToBytes for UserData {
fn to_bytes(&self) -> Vec<u8> {
self.first_name.as_bytes().to_vec()
}
}
impl FromBytes for UserData {
fn from_bytes(bytes: &[u8]) -> Result<Self, typelock::Error> {
Ok(Self {
first_name: String::from_utf8(bytes.to_vec())?,
})
}
}
```
You also have the choice to enable the `postcard-codec` feature, which lets you gain access to the ToBytes and FromBytes derive macros. As long as your struct also derives from [serde's](https://crates.io/crates/serde) `serde::Serialize` and `serde::Deserialize`, these macros automatically generate the necessary to_bytes and from_bytes implementations for you, removing the need to write them manually. This features leverages [postcard](https://crates.io/crates/postcard) under the hood.
## Security providers
To stay flexible, `typelock` is intentionally backend agnostic, meaning you will have to provide your own implementation to encrypt/decrypt/hash your data. For that `typelock` provides the following traits:
```rust
pub struct MySecurityProvider;
// CryptProvider for encrypted fields
impl CryptProvider for MySecurityProvider {
fn encrypt(&self, data: &[u8]) -> std::result::Result<Vec<u8>, crate::Error>;
todo!("implement your own encryption logic here.")
}
fn decrypt(&self, data: &[u8]) -> std::result::Result<Vec<u8>, crate::Error>;
todo!("implement your own decryption logic here.")
}
}
// HashingProvider for hashed fields
impl HashingProvider for MySecurityProvider {
fn hash(&self, data: &[u8]) -> std::result::Result<Vec<u8>, crate::Error>;
todo!("implement your own hashing logic here.")
}
}
```
`typelock` **does not** implement encryption or hashing itself, you will always have to provide **your own** cryptography.
## Converting between models
`typelock` automatically provides the following model conversions:
1. OriginalModel -> LockedModel // call `.lock()`
2. LockedModel -> UnlockedModel // call `.unlock()`
3. UnlockedModel -> LockedModel // call `.lock()`
You are encouraged to always instantiate your own models starting from OriginalModel and never from LockedModel or UnlockedModel. This is because otherwise `typelock` is not able to guarantee that you actually secured your data correctly.
```rust
use typelock::{Lockable, Unlockable};
let provider = MySecurityProvider;
let user = User {
id: 1,
email: "user@email".to_string(),
password: "user-password".to_string(),
user_data: UserData {
first_name: "user".to_string(),
},
};
let locked_user = user.lock(&provider);
let unlocked_user = locked_user.unlock(&provider).expect("implement error handling");
```
Calling `.lock()` and `.unlock()` move the previous model into the function call. This means that trying to access `user` after calling `.lock()` is not allowed. `typelock` also provides default implementations to call `.lock()` and `.unlock()` on collections of Vec<Model>.
**Warning:** you are discouraged from deriving Clone on your **original model** or any of the **generated model's**. This is because doing the following could lead to ghost copies that stay in your programm's memory, which could compromise data.
```rust
let user = User { ... }
let locked_user = user.clone().lock();
```
In cases you need to do so, please take a look at the [zeroize crate](https://crates.io/crates/zeroize) for example.
## Why use Typelock?
Without `typelock`, you typically end up writing code that looks like the following:
```rust
DecryptedUser {
id: self.id,
email: provider.decrypt(...),
...
}
```
This is verbose, easy to get wrong and typically repeated among multiple models. `typelock` generates that bondary once, correctly.
## Use cases
`typelock` is useful when you need:
- Type-safe boundaries between encrypted/decrypted or hashed data states
- Compile-time guarantees that sensitive data can't be accidentally used in the wrong state
- Automatic transitions between domain models and their storage representations
- To eliminate boilerplate for encrypt/decrypt/hash operations across multiple models
- To prevent accidentally serializing or logging sensitive data in plaintext form
- A strict separation between domain logic and persistence layer with security enforced at compile time
`typelock` complements your cryptography library by modeling security state transitions at the type level. It **does not** implement encryption or hashing itself.
## Status
`typelock` is still in an early-stage. Therefore some features are still missing and the API might change.
## License
This project is licensed under the MIT License.
This crate provides optional integration with [serde](https://crates.io/crates/serde) and [postcard](https://crates.io/crates/postcard) via the postcard-codec feature. Therefore the respective licenses (MIT/Apache-2.0) of these dependencies apply.