Generic serializable container for storing cryptographic keys in a key store

Hey folks,

in an attempt to write a store for cryptographic keys, I came up with a Keystore implementation that abstracts most of the logic for saving keys in memory and removing them.

Here is the link to the corresponding Rust playground. I tried to reduce the example as much as possible. Unfortunately, some of the crates used are not available in the playground.

Therefore it uses a few layers of abstraction:

  • a RawKeystore, which is just a hashmap that contains the keys with associated information
  • a ShieldedKeystore, which contains the RawKeystore serialized into a byte format and encrypted with a crate called shielded - it is used when no keys are currently read (the keystore is at rest)
  • an UnshieldedKeystore, that is used to access the keys via the RawKeystore

I came up with the UnshieldedKeystore that holds a reference to its shielded counterpart and contains the RawKeystore, because I had to manually overwrite the encrypted memory each time I performed an operation on the RawKeystore. If I would not overwrite it, all changes would be lost and since it is an operation you forget easily, that part was prone to errors.
Currently, the Drop implementation for the UnshieldedKeystore takes care of refreshing the ShieldedKeystore with the new contents.

I thought this could be a cool little library - the idea is to hide all the details and only expose a public Keystore which again abstracts the other key stores.
You feed a KeyContainer to the KeyStore and it saves it. You can the access it by id and get it back.

A KeyContainer looks like this. It is (de-)serializable.

pub struct KeyContainer {
    key_id: Uuid,
    key: String,
}

I now want to add custom attributes to it (since it should be a general purpose library, there could be custom attributes or there are none). I imagine something like this:

pub struct KeyContainer<'de, T> where T: Debug + Clone + Serialize + Deserialize<'de> {
    key_id: Uuid,
    key: String,
    custom_attributes: Option<T>
}

where T is something like this:

#[derive(Debug, Clone, Serialize, Deserialize)]
struct CustomAttributes {
    a: String,
}

I already tried to add these generic params to all of my other structs (as they all somehow contain the same T).
However, I run into lifetime issues with the lifetime of Deserialize and the other structs. Maybe my approach is not the right one - I feel like I throw the lifetimes around not really thinking about what they mean.
Here is another playground with an intermediate version that doesn't compile because of the lifetime issues I mentioned.

I would really appreciate someone helping me out.

I came up with the UnshieldedKeystore that holds a reference to its shielded counterpart and contains the RawKeystore, because I had to manually overwrite the encrypted memory each time I performed an operation on the RawKeystore.

I suspect this your lifetime issue. Unless I read too fast, this means that there are multiple references to data structures which would need to serialize each other, or to be serialized themselves twice.

Serde requires everything to be in a simple tree it can call impl Serialize trait fns on from top to bottom in a single set of calls. This has to do with the Visitor in the generated code (i.e. by the derive macro).

I would approach your problem differently, using Serde's API itself.

Consider an "encryption" based on base64 (bear with me), a transform that is about the format of the data only. How would you do that without inventing anything?

  1. Create a custom Visitor for your data type, rather than using the default one by the derive macro
  2. Have that Visitor write out your store as a map, except instead of copying the string value the way that visit_map does by default, it would call base64 on it first, then visit the that.
  3. Have a custom Deserialize and Serialize that uses your visitor instead of the procedural macro generate one for you.
  4. Have your code use whatever Serde backend you want -- JSON, bincode, etc. -- by doing bincode::serialize(&store) -- in which case the base64 will just happen because your visitor is used.

Since the encryption is supposed to be transparent while you are accessing the data, I think this approach would also work for you. Just have your "shielded" be the serialized form only, and your "unshielded" be the raw form. The "unshielded" form would have all this and write only to a "shielded" form, which can use the Serde derive macros (because the data is an array of raw bytes at that point).

You'd need to probably do a bit more work on the Deserializer -- e.g. how do you load the encryption key? -- but the Serde interface is flexible enough to make that possible.

You can refer to the serde doc example for custom map deserializers for a full example. Hopefully thinking about the layers will help you understand what it's doing.

Hope that helps!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.