Rotating Masks Across Streaming Releases

A coordinate mask is unbreakable and trivially defeated. Unbreakable, because a 128-bit mask over a prime field is information-theoretically hiding for a single release. Trivially defeated, because two releases of the same coordinate under the same mask subtract to the difference of the plaintexts — and if one plaintext is known, or the coordinate did not move, the mask cancels out entirely. In a streaming pipeline, where the same device is masked thousands of times a day, mask rotation is not housekeeping; it is the protocol. This page specifies it, under coordinate masking protocols in Secure Multi-Party Computation in Spatial Analytics.

Parameter Configuration and Calibration

  • derivation — masks from a KDF, never from a counter or a clock. Derive each mask as m=KDF(kepoch,release_idrecord_id)m = \mathrm{KDF}(k_\text{epoch}, \text{release\_id} \| \text{record\_id}). This makes masks reproducible for authorised reconstruction, unpredictable to everyone else, and — crucially — unique per (release, record) pair without a stateful counter that can be reset.
  • release_id — unique per emitted release, including retries. A retried release that reuses its identifier reuses its mask, which is correct only when the retry is a byte-identical replay of a transmission that never landed. Anything recomputed gets a new identifier.
  • epoch_key_lifetime — how long the epoch key lives. The epoch key is the compromise blast radius: an attacker who obtains it can derive every mask in that epoch. Rotate it on a fixed schedule and destroy the old one once the reconstruction window has closed.
  • reconstruction_window — how long masks must remain derivable. Masks must survive long enough for legitimate reconstruction and not one hour longer. This window is what a deletion claim rests on: once the epoch key is destroyed, the masked records are unrecoverable by anyone.
Failure Mechanism Consequence
Same mask, two releases counter reset, cached mask difference reveals plaintext delta
Same mask, stationary device mask keyed on position difference is zero — device is stationary
Predictable mask PRNG seeded at start-up mask recoverable by replay
Mask logged debug output, crash dump protocol reduces to obfuscation
Probability that some pair of releases shares a mask Two curves against the number of releases in an epoch, on a logarithmic axis. Rotating only per epoch means every release inside it shares a mask, so the probability is a hundred percent throughout. Rotating per release drives it toward zero. The difference is the difference between a working protocol and plaintext. Probability that some pair of releases shares a mask within one epoch, as the release count grows 10 100 1000 10000 100000 0 50 100 releases in the epoch % chance of a shared mask rotate per epoch only rotate per release
Rotating the epoch key is not rotating the mask. Every (release, record) pair needs its own derivation, or the difference attack is available inside the epoch.

Reference Implementation

python
from __future__ import annotations

import hashlib
import hmac
import secrets
from dataclasses import dataclass, field
from typing import Mapping

FIELD = 2 ** 127 - 1


class MaskReuse(RuntimeError):
    """Raised when a (release, record) pair would be masked twice."""


@dataclass
class MaskDeriver:
    """Deterministic, per-release mask derivation with reuse detection.

    Determinism is what lets an authorised party reconstruct without storing every
    mask; the reuse check is what stops determinism from becoming a vulnerability.
    """

    epoch_key: bytes
    epoch_id: str
    _issued: set[tuple[str, str]] = field(default_factory=set)

    def mask(self, release_id: str, record_id: str) -> int:
        key = (release_id, record_id)
        if key in self._issued:
            raise MaskReuse(
                f"mask already issued for release {release_id!r}, record {record_id!r} — "
                "a recomputed release must take a NEW release id"
            )
        self._issued.add(key)
        material = hmac.new(
            self.epoch_key,
            f"{self.epoch_id}|{release_id}|{record_id}".encode(),
            hashlib.blake2b,
        ).digest()
        return int.from_bytes(material, "big") % FIELD

    def rotate(self, new_epoch_id: str) -> None:
        """Start a new epoch with a fresh key; old masks become underivable."""
        self.epoch_key = secrets.token_bytes(32)
        self.epoch_id = new_epoch_id
        self._issued.clear()


def mask_coordinate(value_scaled: int, mask: int) -> int:
    """Additive mask over the prime field."""
    return (value_scaled + mask) % FIELD


def unmask_coordinate(masked: int, mask: int) -> int:
    return (masked - mask) % FIELD


def difference_attack(masked_a: int, masked_b: int, mask: int) -> int:
    """What an adversary computes when two releases share a mask.

    Present so the failure is testable, not because it should ever be reachable:
    with a shared mask the difference of two masked values IS the difference of the
    plaintexts, and for a stationary device that difference is zero.
    """
    return (masked_a - masked_b) % FIELD

Validation Checkpoint

python
def _validate() -> None:
    deriver = MaskDeriver(epoch_key=secrets.token_bytes(32), epoch_id="2026-W33")
    lat_scaled = 51_507_351          # 51.507351 deg at 1e6 fixed point

    # 1. Masking then unmasking with the same derivation round-trips exactly.
    m1 = deriver.mask("r-001", "dev-9")
    assert unmask_coordinate(mask_coordinate(lat_scaled, m1), m1) == lat_scaled

    # 2. Re-issuing the same (release, record) pair must raise, not return the mask.
    try:
        deriver.mask("r-001", "dev-9")
    except MaskReuse:
        pass
    else:
        raise AssertionError("mask reuse must be detected at derivation time")

    # 3. Different releases of the SAME record get independent masks.
    m2 = deriver.mask("r-002", "dev-9")
    assert m1 != m2

    # 4. With distinct masks, the difference of two masked values reveals nothing:
    #    it is not the plaintext difference.
    a = mask_coordinate(lat_scaled, m1)
    b = mask_coordinate(lat_scaled, m2)
    assert difference_attack(a, b, m1) != 0

    # 5. With a SHARED mask, a stationary device is exposed exactly — the attack.
    shared = deriver.mask("r-003", "dev-1")
    c = mask_coordinate(lat_scaled, shared)
    d = mask_coordinate(lat_scaled, shared)
    assert difference_attack(c, d, shared) == 0, "shared masks reveal stationarity"

    # 6. Rotation must make old masks underivable.
    old_key = deriver.epoch_key
    deriver.rotate("2026-W34")
    assert deriver.epoch_key != old_key
    assert deriver.mask("r-001", "dev-9") != m1

    # 7. Derivation must be reproducible for an authorised party holding the key.
    twin = MaskDeriver(epoch_key=deriver.epoch_key, epoch_id=deriver.epoch_id)
    assert twin.mask("r-777", "dev-3") == MaskDeriver(
        epoch_key=deriver.epoch_key, epoch_id=deriver.epoch_id
    ).mask("r-777", "dev-3")

    print("mask rotation: all assertions passed")


_validate()
What a shared mask reveals A grouped bar chart of three scenarios. With distinct masks nothing is revealed. With a shared mask the difference of the two masked values is exactly the difference of the plaintexts, so a device's displacement is revealed in full. If the device did not move, the two masked values are identical and stationarity is revealed directly. What a shared mask reveals two releases of the same record 0 50 100 % 0 0 distinct masks 100 0 shared mask, moved 400 m 100 100 shared mask, stationary plaintext difference revealed (%) exact position revealed (%)
The stationary case needs no arithmetic at all: two identical masked values announce that the device has not moved.

Assertion 5 is the attack in three lines, and it is worth running in CI precisely because it looks like a trivial test. A device that has not moved produces two identical masked values under a shared mask — the adversary does not even need to compute a difference to notice.

Incident Response and Edge Cases

  • A release is retried after a failure. If the payload is byte-identical and never landed, reuse the release id and the mask; if anything was recomputed, issue a new id. Encode this in the retry path rather than leaving it to the operator, because under incident pressure the instinct is to replay.
  • Two pipeline instances mask the same record. Both derive from the same epoch key and the same record id, so the reuse detector in one process cannot see the other’s issuance. Partition the release-id space per instance — a prefix is enough — so collisions are impossible by construction rather than by coordination.
  • The epoch key leaks. Every mask in that epoch is derivable, so every masked record in it is plaintext to the holder. Rotate immediately, re-mask anything still within its reconstruction window under a new key, and treat the old epoch’s records as disclosed.
  • A mask appears in a log line. That record is compromised, and probably others: whatever printed one mask will have printed many. Audit the log path, purge, and add a test that the mask type has no string representation containing its value.
  • Reconstruction is needed after the epoch key was destroyed. By design, impossible. This is the property that makes the deletion claim true, so the answer is to widen the reconstruction window going forward, not to retain keys “just in case”.
The mask lifecycle, and what each shortcut costs A three-row table covering mask generation, rotation and destruction. Each row contrasts a common weak practice with the correct one and names the resulting failure: a process-level PRNG repeats masks across releases, daily rotation lets same-day releases share a mask, and retained keys make a deletion claim untrue. The mask lifecycle, and what each shortcut costs all three failures are engineering, not cryptography weak practice correct practice failure if weak Generation process PRNG at start-up KDF per release masks repeat across releases Rotation daily per release same-day releases share a mask Destruction retained "just in case" key destroyed at window end deletion claim is false
None of these is a cryptographic weakness. All three are ordinary engineering, which is exactly why they are the ones that ship.

A useful review question for any masking pipeline: name every place a mask could be persisted, and show the test that proves it is not. The list is longer than it first appears — application logs, crash dumps, metrics labels, tracing spans, debugging fixtures, and the test corpus itself — and a mask surviving in any one of them reduces the whole protocol to obfuscation for the records it covers. The derivation design above helps here too: because masks are derived rather than stored, the only long-lived secret is the epoch key, and there is exactly one place to protect it.

Frequently Asked Questions

Why derive masks instead of storing them?

A stored mask table is a second copy of the sensitive data — hold it and you must protect it exactly as you protect the coordinates. Derivation from an epoch key plus a release and record identifier gives reproducibility without storage, and destroying one key destroys every mask in the epoch at once.

Is rotating per epoch enough, or must it be per release?

Per release. An epoch key is the derivation secret, not the mask: within an epoch every (release, record) pair still gets its own unique mask. Rotating only the epoch key while reusing masks inside it leaves the difference attack fully available.

What if the same coordinate is legitimately released twice?

It gets two independent masks, and the two masked values look unrelated — which is the intended behaviour. The consumer that needs to know they are the same record uses the record identifier, which is handled at a different layer with its own linkability decision.

How does mask rotation relate to secure aggregation's masking?

They solve the same problem at different scales. SecAgg's pairwise masks cancel within one round's sum; coordinate masks hide individual records across a stream. Both fail the same way — reuse — and both need per-release derivation with an explicit reuse check.

Up one level: Coordinate Masking Protocols · Section: Secure Multi-Party Computation in Spatial Analytics.