# 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
## 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())?,
})
}
}
```
In the future `typelock` might add extensions to provide a default implementation for `serde` annotated data since it is so popular in the Rust community. However these extensions will always stay optional, meaning if they are added, they will have to be explicitly enabled inside your `Cargo.toml`.
## 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]) -> Vec<u8> {
todo!("implement your own encryption logic here.")
}
fn decrypt(&self, data: &[u8]) -> Vec<u8> {
todo!("implement your own decryption logic here.")
}
}
// HashingProvider for hashed fields
impl HashingProvider for MySecurityProvider {
fn hash(&self, data: &[u8]) -> Vec<u8> {
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.
## Usecases
`typelock` is useful when:
- You store encrypted or hashed blobs in a database
- You want to enforce explicit security boundaries
- You want a strict separation between domain and storage models
- You want domain and storage representations to be structurally related but type-distinct
- You want compile-time guarantees around security state transitions instead of relying on conventions
- You do not want to write transition between models yourself
`typelock` is not intended to replace your cryptography library. It exists to model security states and transitions explicitly at the type level.
## 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.