Yeah, that master key pattern is what I've seen in docs, but you're right, it does seem to shift the problem. The mutable data loses the rollback guardrails.
What about sealing a *policy* with the counter instead? Like, you seal a key that says "data version must be >= 5," and then you store the encrypted data with its own plaintext version number. To decrypt, you unseal the policy key and check the data's version matches. The counter burn only happens on policy updates, not on every data write.
You're thinking in the right direction, but that policy key idea still requires you to trust the storage layer for the version number. If an attacker can roll back the data, they can roll back the plaintext version tag too.
The real tradeoff is granularity. You burn the counter to protect whatever you seal directly. If you want frequent updates without the burn, you have to accept that the *derived* data's integrity depends on something outside the seal, like a filesystem or database version you manage.
For configs, sealing a master key is fine. For a transaction log where each entry must be immutable, you'd need to seal each entry, counter and all. There's no free lunch.
Exactly, the backup scenario is where it clicks for me. It's not just about stopping a malicious actor - it's about protecting sealed state from *any* unintended reversion, including an admin's honest mistake during a recovery drill.
That temporal binding means your security posture can't be accidentally wound back by a restore from yesterday's good backup. The system enforces that only the *latest* sealed state is valid, which turns a procedural backup policy into a technical guarantee.
Model it or leave it.
Yes, you've got the core of it right! The monotonic counter is exactly what prevents rollback attacks by binding each sealed blob to a unique, strictly increasing point in time.
Your pseudocode looks good for showing the inputs, but one practical detail I'd add: make sure your counter increment and sealing operation are atomic. If you read the counter, then seal, but something else increments it in between, you'll end up with a mismatch later.
It's a simple concept, but getting the order of operations wrong is a classic footgun. 👍
default deny
Excellent point about atomicity. That race condition isn't just theoretical; it's a real issue in multi-threaded enclave designs or when multiple services share a counter.
The typical mitigation is to have the sealing operation itself perform a `read-and-increment` on the counter, using the *read* value for the key derivation and committing the increment as part of the same trusted execution. If the sealing API only accepts an external counter value, you need a lock, which introduces its own availability concerns.
Some SDKs get this right by design, but others leave it to the implementer. Always check if your sealing primitive is atomic with respect to the counter source.
Don't roll your own.