Summary
The serde::Serialize
and serde::Deserialize
traits are used pervasively throughout the Rust ecosystem to enable flexible (de)serialization of data. Many authors derive these types on structs using macros provided by serde
. If orion's types don't support these traits, users won't be able to derive these serialization traits, and will have to resort to converting orion types before storing them in a struct for serialization.
By implementing serde
's standard traits, we enable authors to skip the mental burden and increased complexity of serializing orion types without serde.
Scope
At least for a first implementation, I would suggest only implementing serialization traits for types that are expected to either be stored in a database or sent to another application (e.g. over the network). This would include the following types.
pwhash::PasswordHash
hash::Digest
auth::Tag
kdf::Salt
(added after suggestion below)
Motivating Example
My motivating example is storing a User
type in a database. It would be nice to simply define a User
struct, and then have it serialize to one long string using, for example, the bincode
crate. This is what I would like the basic code to look like (using a fictional database API).
use serde::{Serialize, Deserialize};
use orion::pwhash::PasswordHash;
#[derive(Serialize, Deserialize)]
struct User {
username: String,
pwhash: PasswordHash,
last_login: chrono::DateTime<Utc>,
}
fn store_user_in_db(user: &User, db: &Db) {
db.write_bytes(&user.username, bincode::serialize(user));
}
fn retrieve_user_from_db(db: &Db) -> User {
let bytes = db.get_bytes(&user.username);
bincode::deserialize(bytes)
}
The PasswordHash
type would serialize as a string by delegating to PasswordHash::unprotected_as_encoded
.
Without serde
's types implemented, however, we would have to do one of the following.
- Create a separate
User
type that can be serialized.
- Implement
serde
manually on User
and delegate the PasswordHash
serialization to its unprotected_as_encoded
method. Neither solution is particularly attractive.
A separate, serializable User
struct is probably the easiest of the the alternatives. It would look like this.
// previous definitions omitted
#[derive(Serialize, Deserialize)]
struct UserSerialize {
username: String,
pwhash: String,
last_login: chrono::DateTime<Utc>,
}
fn store_user_in_db(user: &User {username, pwhash, last_login}, db: &Db) {
let user_serialize = UserSerialize {username, last_login, pwhash: pwhash.unprotected_as_encoded()};
db.write_bytes(&user.username, bincode::serialize(to_serialize));
}
fn retrieve_user_from_db(db: &Db) -> User {
let bytes = db.get_bytes(&user.username);
let UserSerialize {username, pwhash, last_login} = bincode::deserialize(bytes);
User {username, last_login, pwhash: PasswordHash::from_encoded(pwhash)}
}
It isn't terrible in terms of boilerplate, but it could start to get annoying and possibly error-prone to repeat struct definitions for several different types instead of just User
.
Implementing Serialize
and Deserialize
manually can be… unpleasant. Especially for anything complex, and definitely more boilerplate in any case.
Pros of implementing Serialize
/Deserialize
- There is significant ergonomic benefit to supporting this nearly ubiquitous pair of serialization traits.
- Users may be less likely to mistakenly reach for non-constant-time operations if they never actually (de)serialize the type themselves and just let
serde
do it for them. The common workflow is to deserialize some bytes or a string into the relevant type (would be User
here, for example), and then to do operations on the fully featured type. This is beneficial because many of orion's types use constant-time operations for things like comparisons. It's anecdotal, but in my first example, the PasswordHash
type spends very little time in its serialized state because it's so easy to deserialize into the fully featured type. Making deserialization the fast path could save users from security mistakes.
Cons
- The serialization functions in orion are helpfully named
unprotected_*
to signify that by using them, authors are relinquishing the constant-time features that orion provides. If we hide these functions in a serde::Serialize
implementation, users may be more likely to mistakenly serialize a sensitive type and perform insecure operations on it. I realize that this somewhat contradicts the last "Pro" above, but they both seem like legitimate possibilities to consider.
Additional Considerations
- Serialization should conform to constant-time operations already in place. For example,
PasswordHash
should serialize as a string using the already-implemented PasswordHash::unprotected_as_encoded
function.
- We may have to add additional mechanisms for testing to ensure that all tests pass with or without the hypothetical
serde
feature enabled.
new feature