Excited and new to Rust. As a learning exercise I've been rewriting an application in Rust and struggling to understand how to serialize strings using the secrecy crate.
pub struct VariableRequest<'a> {
pub account_number: SecretString,```
```#[derive(Serialize, Deserialize)]
| ^^^^^^^^^ the trait `SerializableSecret` is not implemented for `std::string::String`, which is required by `Secret<std::string::String>: Serialize```
I imagine I'm overlooking something fundamental about how to implement traits in a scenario like this. If someone can point me at the fundamental knowledge to understand what I'm supposed to implement I'd greatly appreciate it. There is only so much banging my head against the keyboard I can take. :grinning:
https://docs.rs/secrecy/latest/secrecy/
It appears from the documentation that this is a trait indended to be implemented manually for/by types that should support serialization inside a Secret. I think String intentionally doesn't implement it.
Since you can't implement it for String yourself due to coherence rules, you'll need to make your own newtype wrapper around String and implement this trait for that type.
you'll need to make your own newtype wrapper around String and implement this trait for that type.
Does this mean I create a new struct that uses String like the following? If so, once I add SerializableSecret, then it asks for Serialize, then DefaultIsZeroes,...
I'm guessing I don't need to reimplement all those traits so I'm still missing the correct approach.
Oh, you totally do. But there's only 3 of them, and all of them are trivial (Zeroize and Serialize can and should just forward to String, and the latter can even be derived).
BTW, your current Serialize impl is nonsencial as it's circular. The whole reason for introducing the newtype wrapper was that String is not SerializableSecret, so SecretString<String> is not Serialize.
A newtype wrapper should generally just forward to the wrapped type. No tricks involved.
Ah, okay, so we don't actually have to write new implementations for the other traits. We can use the derive attribute and use the compiler provided implementations. What is the purpose of this line then? Simply to signify to the secrecy crate that we opt-in to serialization?
Sorry I don't understand the question. The compiler-derived impls are just as "new" as the ones you write by yourself. Derive macros are not magic, they are just macros that happen to expand to trait impls by convention.
The whole point of a library such as secrecy is to avoid leaking sensitive information. Most of the time, developers leak such information unintentionally, because one can only handle so much information as to always recall not to do X with Y secret value. Examples of this are:
Logging a value: i.e. you start using some library with a declarative API that simply allows you to log all the arguments and returned values of function calls, and by mistake you forget tat User has the AccountNumber which is a sensitive value.
Serializing: Same as before, just about a different trait. Suppose that in your domain, users can be searched, and by mistake you forget to exclude the account_number field from the serialized model.
And the list goes on and on. So, as a way to aid the developer using the library, secrecy intentionally doesn't impl <S: Serialize> Serialize for Secret<S>, you have to consciously opt-in, and that means to manually implement SerializableSecret, which then will forward the serialization to the underlying type.
I guess my semantics meter doesn't view this necessarily as an implementation, just an empty block to signify the opt-in action. I wonder if Rust has other approaches/best practices to opt-in to functionality without this block. Or is this something I just have to get used to in Rust? impl SerializableSecret for AccountNumber {}
One last clarifying question if I may. Does the where clause in the secrecy library indicate the list of requirements necessary to "which then will forward the serialization to the underlying type".
impl<T> Serialize for Secret<T>
where
T: Zeroize + SerializableSecret + Serialize + Sized,
The thing is that Serde in the Rust ecosystem is pretty much omnipresent. The same phenomena happens in other languages, and usually happens around essential things such as logging and de/serialization.
That being the case, when you develop a new library you have to have this in mind, for whatever opt-in mechanism that you choose if you want the usage of your library to be more ergonomic.
In this case, having the user manually implement a marker trait sound pretty reasonable to me. It makes me aware of what I'm doing as a developer, without being too boiler-platey.
Pretty much, yes. The where clause allows you to list all the traits required for a given generic type argument.
In Rust, traits denote properties you can rely on. If there's no executable code attached to that property, the trait is empty. There's nothing special or abhorrent about that.
After all, a secret string is mechanistically exactly the same as a non-secret string: you can get its characters, its length, append to it, slice it, serialize it, whatever. The only difference is "who is supposed to be allowed to look at this", and that's absolutely not something you can achieve (on this level of abstraction) by power-typing tons of executable code.
So there's no good reason to invent special
syntax for empty traits. A pair of braces can contain some items or none; the fewer the better. Since the syntax is already perfectly capable of expressing "mark this trait as implemented, even though there's technically no code in it", why complicate it?
Also note that although the previous answer stated "yes", this is not entirely accurate. Where clauses know nothing about "forwarding" (or any other implementation details, for that matter). Where clauses (and the syntactically distinct but functionally equivalent inline bounds) are exclusively for declaring "if T implements Tr1 then U implements Tr2" type constraints and relationships. That's it, that's all they are capable of doing.
The generics system is concerned with interfaces and constraints, and as such, it operates purely at the level of types and declarations. What those actually do is none of the generics' business.
As far as the type checker is concerned, you could implement all those traits in an arbitrary (and entirely nonsensical) manner. You could make Zeroize a no-op and screw up security big time; you could serialize an AccountNumber as a pre-defined, constant, invalid account number instead of the wrapped string; you could even implement all the traits you can imagine by panicking in every method, and the compiler would still be pleased. Because the compiler only knows what you wrote, not what you wanted to happen.
How a trait is actually implemented is entirely a question of common sense and domain knowledge. If the compiler could infer something like "hey this looks like a wrapper, and based on what they are supposed to do, we should just forward traits X, Y, and Z", then we would have reached the pinnacle of AI and wouldn't need programmers any more.