I like the idea of this thread! Maybe we can go even further and also include things like best practices, pattern / anti-pattern, common pitfalls, etc. - building something on Autonomi is still largely uncharted territory at this point.
I’ll start with something:
Typed, hard to misuse keys
(Rust specific)
In Ark, the project I am currently working on, I have do deal with a number of keys for different roles / functions / responsibilities. To make things more interesting, I am also dealing with key hierarchies, data that is owned by one key but readable with another, key rotations …
Long story short, things could get messy easily. I ended up with type system I am really happy with and is really clean to work with:
I use zero-sized marker types to define a context / purpose, eg:
pub struct ArkRoot;
pub struct HelmKeyKind;
pub struct WorkerKeyKind;
...
Base Key types with common functionality (excerpt):
#[derive(Zeroize, Debug, Clone, PartialEq, Eq)]
pub struct TypedSecretKey<T> {
inner: SecretKey,
...
}
impl<T> TypedSecretKey<T> {
...
pub fn public_key(&self) -> &TypedPublicKey<T> {
...
}
pub(super) fn decrypt<V: for<'a> TryFrom<&'a [u8]>>(
&self,
input: &EncryptedData<T, V>,
) -> anyhow::Result<V>
where
for<'a> <V as TryFrom<&'a [u8]>>::Error: Display,
{
...
}
pub(super) fn derive_child<C>(&self, idx: &TypedDerivationIndex<C>) -> TypedSecretKey<C> {
...
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TypedPublicKey<T> {
inner: PublicKey,
_type: PhantomData<T>,
}
...
Most notably here, decrypt
and derive_child
are not public - they are supposed to be used by more domain specific functions that are public, eg:
pub type HelmKey = TypedSecretKey<HelmKeyKind>;
impl HelmKey {
pub fn worker_key(&self, seed: &WorkerKeySeed) -> WorkerKey {
self.derive_child(seed)
}
...
}
Type aliases
are really great here. The fact we can have an impl
for a specific type alias and basically treat it like its own type feels almost like cheating. I can easily make sure certain operations are only possible for/with certain types and its enforced by the compiler. Its much easier to reason about and virtually eliminates an entire class of crypto-related bugs.
An example with typed encrypted data:
pub struct EncryptedData<T, V> {
inner: Ciphertext,
_type: PhantomData<T>,
_value_type: PhantomData<V>,
}
pub type EncryptedManifest = EncryptedData<WorkerKeyKind, Manifest>;
pub type WorkerKey = TypedSecretKey<WorkerKeyKind>;
impl WorkerKey {
pub fn decrypt_manifest(
&self,
encrypted_manifest: &EncryptedManifest,
) -> anyhow::Result<Manifest> {
self.decrypt(encrypted_manifest)
}
}
pub type PublicWorkerKey = TypedPublicKey<WorkerKeyKind>;
impl PublicWorkerKey {
pub fn encrypt_manifest(&self, manifest: &Manifest) -> EncryptedManifest {
self.encrypt(manifest.clone())
}
}
If you have a worker_key
your can call the decrypt_manifest
function and get a fully decrypted Manifest
back:
worker_key.decrypt_manifest(&encrypted_manifest)
decrypt_manifest
will not be available on any other key type or for any other encrypted data type. And this is fully enforced by the compiler and supported by any IDE. Neat!
This is just to give an idea whats possible. I’ve explored this concept much further in my project. More code here.