hanki

secret

stdlib/core/secret.hk: Secret, material the renderers refuse to print.

A private key or a token leaks through the machinery that exists to be helpful. dbg! and the crash-time actor-state dump render a value structurally, from its layout, and leaving Display off a type does not stop either of them. A derived Encode walks every field, and a serialised config would hold the key with nothing at the call site to say so. And == on bytes returns at the first difference, and the obvious comparison hands back the material one byte at a time.

Wrapping the material in a Secret closes those three. The renderers print <redacted> from the layout alone, a derive refuses the field outright in place of including it unannounced, and there is no Eq to reach for.

Reading the material back is reveal, and it is meant to be the greppable act it looks like: one call site per place the secret has to be used.

Comparing two secrets means revealing both and comparing in constant time, through sha2.constant_time_eq? in extra. It takes bytes, and a Secret<string> needs .to_bytes() on each revealed side.

Three limits are real, and stated here:

Secret

opaque Secret<T>
  value: T
end

Material that must not be rendered, serialised, or compared by accident. T is the material's own type: bytes for a private key, string for a token. A Secret has no traits at all, and an aggregate holding one cannot derive Display, Encode, Eq, or Hash either.

impl<T> Secret<T>

hide

def hide(value: T) -> Secret<T>

Wrap value where the renderers and the derives cannot reach it.

Secret.hide("hunter2").reveal() => "hunter2"

reveal

def reveal(self) -> T

The material itself. Every call marks a place the secret exists in the clear, which is the reason it is spelled out and never implicit.

Secret.hide(42).reveal() => 42