Skip to content

FIND-004 - Indexer - Viewing keys exposed to indexer operators #245

@TheNewAutonomy

Description

@TheNewAutonomy

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (High)

Description

The midnight-indexer API and implementation require users to provide their ViewingKey to the indexer when connecting a wallet. The indexer uses the viewing key to derive a secret key and then attempts to decrypt transaction outputs to determine which shielded transactions belong to that wallet.

This design treats the indexer as a trusted component. Operators running public or third-party indexer instances would be able to observe viewing keys (in transit and at rest) and decrypt or correlate users' shielded transaction data, breaking the privacy guarantees expected by shielded transactions.

Importantly, indexers are treated as a core component and are enforced during setup in the Lace wallet. That means users are guided or required to include an indexer (often the project's first-party indexer) in their onboarding flow. In that scenario, Midnight itself becomes the primary vector capable of receiving ViewingKeys and therefore of leaking private transaction data. After Midnight ships and users begin connecting to third-party indexer providers, the same exposure expands: privacy leakage that initially centralizes trust in Midnight will be propagated to any third-party indexer operators users connect to, multiplying the potential points of compromise.

Impact

Loss of user privacy: viewing keys allow decryption of shielded outputs, so a malicious or compromised indexer operator can read otherwise-private transactions.

Linkability: operator can correlate addresses, transactions, and activity across wallets connecting to their indexer instance.

Trust assumptions: users may assume privacy properties of shielded transactions; this design shifts trust to indexer operators and operator-managed infrastructure.

Proof of Concept

This finding is derived directly from the public documentation, the repository public code (midnight-indexer), and the observed behavior on the testing environment at indexer.preprod.midnight.network

As seen below, in order to connect to the indexer with a wallet, and access some of its functionality, a viewingKey must be provided during the connection phase (documentation reference). This allows the user to query and subscribe to blockchain data: blocks, transactions, contracts, and wallet-related events indexed from the Midnight blockchain.

Image

Once the ViewingKey is shared to the indexer, it effectively transfers the ability to read and decipher shielded transactions. This is expected on the current implementation, but forces the user to forfeit one of the principal privacy features that midnight provides right at the start.

Since the indexer service stores an encrypted viewing key in the database and holds the encryption key (cipher) in process memory/config, an operator with access to the host or database backups can simply decrypt stored viewing keys using their configured local encryption key.

With the viewing key the operator can call key.decrypt(...) on transaction ciphertexts to determine which outputs belong to the wallet, enabling full transaction linkability and content disclosure.

Code references

The snippets below from the indexer public code showcase how viewingKeys are handled and used:

https://github.com/midnightntwrk/midnight-indexer/

indexer-common/src/domain/ledger/transaction.rs

The transaction.relevant(viewing_key: ViewingKey) path deserializes the viewing key into a SecretKey via viewing_key.expose_secret().

The code calls SecretKeyV7_0_0::from_repr(&viewing_key.expose_secret().0) and then uses key.decrypt(...) on transaction ciphertexts to find matches.

Lines: 196-225 (relevant), 266-278 (can_decrypt_v7_0_0)

indexer-common/src/domain/viewing_key.rs

ViewingKey stores the secret and provides expose_secret(), encrypt(), decrypt(), and to_session_id(). It explicitly warns not to leak the secret. (lines: 27-37, 34-37)

indexer-api/src/infra/api/v3/viewing_key.rs

API type ViewingKey::try_into_domain parses bech32m input and converts to domain ViewingKey, exposing the secret for use by domain logic (lines: 30-52).

indexer-api/src/infra/storage/wallet.rs

connect_wallet stores the viewing key encrypted at rest in the indexer's wallets table. The viewing key is encrypted using ChaCha20-Poly1305 and the server-side cipher before insertion (lines: 21-50).What an indexer operator can do

Recommendation

Short-term (low-effort)

Make the trust boundary explicit in documentation and UI: clearly state that connecting a wallet to an indexer requires sharing the viewing key and that the indexer operator can decrypt associated transactions.

Encourage users to run their own private indexer instances or provide an option to only connect to local/private indexers.

Medium-term (architectural)

Move decryption to the client: instead of sending viewing keys to indexer, have the indexer provide blinded or encrypted blobs and let the client perform decryption locally to determine relevance. Indexer can index and store encrypted blobs only; clients then pull blobs and filter locally.

Use privacy-preserving protocols: design an approach where the indexer can assist with indexing (e.g., store envelopes) but cannot learn transaction ownership.

Introduce ephemeral session tokens bound to a hardware-protected key: if the indexer must perform decryption, use an architecture where the decryption key material is held in a hardware module or ephemeral secret that the operator cannot export.

Although these mitigations will likely introduce measurable performance overhead for the indexer, it is imperative that protocol-operated components preserve fundamental privacy guarantees. Ensuring that core infrastructure upholds these guarantees is essential to maintaining user trust and the long-term adoption of the platform.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions