You own a Go library wrapping hkdf.Key for ten teams — how do you shape the info-string namespace you can never change?
answer
- the label is a contract, not an argument
- changing it re-derives every key
- decide who may invent one
- the version suffix is the rotation lever
basics
~20 sTreat info strings as a versioned public namespace — owner, purpose, algorithm, version — allocated centrally, and export named accessors rather than a raw string parameter. Changing a label later means re-deriving everything that key protects.
solid answer
~50 sThe `info` string is the only part of `hkdf.Key` a caller chooses, and it is load-bearing: change it and the key changes, so every artefact already protected by the old key has to be re-derived and rewritten. That makes it a published contract, not a parameter. So I would not export `Key(purpose string, n int)` at all — a free-form string invites ten teams to invent ten conventions, and the first duplicate is silent. The library exports one method per allocated purpose, and adding a purpose is a pull request against my package that a reviewer sees. The label itself carries owner, purpose, algorithm and an explicit `v1`, because that version suffix is the only rotation lever HKDF gives you when the master secret cannot change. Where I concede: teams needing per-tenant keys get a constructor scoped to a namespace I allocated, not free-form strings.
code
go · 13 lines// Rejected: a free-form label — ten teams, ten conventions, silent duplicates.
type Deriver interface {
Key(purpose string, n int) ([]byte, error)
}
// Shipped: one method per allocated purpose, so a new key is a pull request.
type Keys interface {
SessionCookieKey() ([]byte, error)
DownloadURLKey() ([]byte, error)
// Concession: dynamic keys, but only under a namespace I allocated.
TenantKey(ns Namespace, tenantID string) ([]byte, error)
}go deeper
Know that the info string decides which key you get, so the label in your code is not decoration. Copying another team's label gives you their key, not a similar one.
Explain why a version suffix belongs in the label from the start, and what re-deriving would cost once real data is already protected by the key under the old label.
Be ready to run the migration: derive both labels, accept either on read, write only the new one, retire the old when you can prove nothing still reads it.
Argue the API shape against real consuming teams — named accessors versus a string parameter, and the friction that costs — and say what you concede for dynamic per-tenant keys without reopening the namespace.
## Why this is a design decision and not a coding one `hkdf.Key(h, secret, salt, info, keyLength)` has exactly one argument that varies per caller in a shared library: `info`. And HKDF's defining property is determinism — the same inputs always yield the same bytes. Put those together and the consequence is uncomfortable: **the info string is part of the key's identity, so it can never be changed once anything is protected by the resulting key.** That is what makes this a namespace design rather than a naming convention. A field name in a struct can be renamed with a refactor. An info string cannot: renaming it produces a different key, and everything sealed, signed or authenticated under the old key stops verifying. Correcting a label after release is a data migration measured in weeks, not a pull request. ## The API shape is the whole argument The tempting export is the general one: ```go func (d *Deriver) Key(purpose string, n int) ([]byte, error) ``` It is flexible, it needs no changes when a new team arrives, and it is the wrong answer for a library ten teams depend on. Three failure modes follow from it: 1. **Ten conventions.** One team writes `"session"`, another `"acme/session-cookie/aes-256/v1"`, a third `"SessionCookie"`. Nothing enforces a shape, so none emerges. 2. **Silent duplicates.** Two teams that pick the same obvious word share a key, and the package raises no error. There is no compile-time or run-time signal. 3. **No review point.** Allocating a new key never touches my package, so nobody with the whole namespace in view ever sees it happen. The alternative gives up flexibility on purpose: ```go type Keys interface { SessionCookieKey() ([]byte, error) DownloadURLKey() ([]byte, error) } ``` Now a caller cannot express the mistake. Adding a purpose means editing my package, which means a pull request, which means someone reviews the label before the first key derived from it exists. That is the entire point: I am buying a review gate at the cost of a code change per new purpose, and for something irreversible that trade is worth making. The honest cost of this choice is friction. A team that needs a key on Friday now waits for my review. I would rather absorb that than absorb a namespace collision, but it is a real objection and one a consuming team can legitimately push back on — which is why the decision belongs to someone who can hear the pushback and still hold the line, or change it deliberately. ## What goes in the label Four components, and each earns its place: - **Owner** (`acme/billing/`) — makes the namespace partitionable and makes the duplicate check meaningful across teams. - **Purpose** (`session-cookie`) — what the key is for, in the domain's words. - **Algorithm or key shape** (`aes-256`, `mac`) — so a key intended for one primitive is not quietly reused for another when the code evolves. - **Version** (`v1`) — the rotation lever. The version suffix is the part teams leave out and regret. When the master secret cannot be changed — because it is shared by every purpose and rotating it rotates everything at once — bumping `v1` to `v2` is the only way to get a fresh, unrelated key for one purpose. If the label has no version from day one, adding one later is itself the migration you were trying to avoid. ## The concession worth making Some needs are genuinely dynamic: a key per tenant, per shard, per customer-supplied encryption context. Named accessors cannot enumerate those. Refusing the case entirely pushes teams to build their own deriver next to mine, which is strictly worse — now there are two namespaces and no review of either. So I would expose a constrained constructor: the caller supplies the variable part, but the library prefixes the allocated namespace and the version. The dynamic segment is never the whole label, and the review still happened once, when the namespace was allocated. ```go func (d *Deriver) TenantKey(ns Namespace, tenantID string) ([]byte, error) ``` ## The alternative I would reject Giving each team its own master secret sounds like it removes the coordination problem, and it does — by replacing it with a worse one. Now there are ten secrets to provision, store, rotate and audit instead of one, ten opportunities to generate a weak or duplicated value, and no central view of what exists. One master secret plus a policed info namespace keeps the protected-storage surface at a single item while still giving cryptographically independent keys. The coordination cost is the price of that, and it is the cheaper price. ## Environment scoping One question that has to be settled library-wide rather than per purpose: does the environment name belong in the label? Including it makes staging and production keys unrelated by construction, which is excellent for blast radius and means a leaked staging secret buys nothing. It also means no encrypted artefact can be promoted between environments, and any workflow that copies production data down to staging stops working. Pick one answer and apply it uniformly — a namespace where some purposes are environment-scoped and others are not is the worst of both, because nobody can predict which is which.
- How do you rotate one purpose's key when the master secret cannot change?Bump the version suffix in the info string — `v1` to `v2` — which HKDF turns into an unrelated key from the same secret. Then run the usual dual-read migration: derive both, accept either on read, write only the new one, retire `v1` when nothing reads it. That is exactly why the version belongs in the label from day one rather than being appended after the fact.
- Why not give each team its own master secret instead of policing one namespace?It trades one coordination problem for a worse operational one: ten secrets to provision, store, rotate and audit instead of one, and no central view of what exists. A single master plus an allocated info namespace keeps the protected-storage surface at one item while still yielding cryptographically independent keys, provided the namespace is genuinely reviewed.
- A team wants the environment name inside the info string. Do you allow it?Only as a library-wide decision. Including it makes staging and production keys unrelated by construction, which limits blast radius but means no encrypted artefact can be promoted between environments and any data-copy-down workflow breaks. What I would not accept is some purposes being environment-scoped and others not, because then nobody can predict which is which.
saying these in an interview costs you the question
- Exports a free-form purpose string and documents a convention
- Thinks an info string can be renamed later at no cost
- Omits a version suffix and plans to add one when needed
- Lets each consuming team choose its own label format
- Rotates one purpose by regenerating the shared salt