Encryption & Key Management
Every cryptographic operation in the Specter data protocol happens client-side. The chain stores commitments, nullifiers, and encrypted key fragments. It never sees plaintext data, unencrypted private keys, or user passphrases. This page documents every cryptographic primitive used in the protocol, how they compose, and why each was chosen.
Cryptographic Primitives
| Primitive | Algorithm | Use | Security Level |
|---|---|---|---|
| Commitment hashing | Poseidon (BN254) | Merkle tree commitments, nullifier derivation, accessTag computation | ~127-bit (BN254 field) |
| Zero-knowledge proofs | Groth16 on BN254 | Proof of commitment knowledge + Merkle membership | 128-bit (BN254 pairing) |
| Symmetric encryption | AES-256-GCM | Passphrase protection of PNG metadata, voucher encryption | 256-bit |
| Password-based KDF | PBKDF2-SHA256 | Passphrase to AES key derivation | Configurable (iterations) |
| Seed-based KDF | HKDF-SHA256 | Seed to commitment secrets derivation | 256-bit (SHA-256) |
| Split-key encryption | XOR (one-time pad) | Private key splitting for Phantom Identity | Information-theoretic |
| Quantum commitment | keccak256 | Post-quantum commitment rotation | ~128-bit post-quantum |
| Key binding | keccak256 | keyId derivation (commitment + issuer binding) | 256-bit |
| NFC encryption | AES-128 (NTAG 424 DNA) | On-chip tag encryption | 128-bit |
| NFC authentication | AES-CMAC (SUN) | Tag authenticity verification | 128-bit |
AES-256-GCM: Symmetric Encryption
AES-256-GCM is used wherever data needs to be encrypted at rest with a known key: passphrase-protected PNG metadata and encrypted voucher payloads.
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Key size | 256 bits | Derived via PBKDF2 (passphrase) or HKDF (seed) |
| IV size | 96 bits (12 bytes) | Random, unique per encryption |
| Auth tag size | 128 bits | Appended to ciphertext |
| Mode | GCM (Galois/Counter Mode) | Authenticated encryption (confidentiality + integrity) |
| Implementation | Browser WebCrypto API | crypto.subtle.encrypt({name: "AES-GCM", ...}) |
Why GCM?
GCM provides authenticated encryption — it protects both confidentiality (the data is unreadable without the key) and integrity (any modification of the ciphertext is detected). This prevents an attacker from tampering with encrypted PNG metadata without detection. A non-authenticated mode like AES-CTR would encrypt the data but would not detect if an attacker flipped bits in the ciphertext.
Security Properties
| Property | Guarantee |
|---|---|
| IND-CCA2 | Ciphertext is indistinguishable from random data under adaptive chosen-ciphertext attack. |
| Integrity | Any modification to the ciphertext (including truncation) is detected via the auth tag. |
| IV uniqueness | Random 96-bit IV. Probability of collision is negligible for practical usage volumes ( encryptions per key). |
AES-GCM is catastrophically broken if the same IV is used twice with the same key. The protocol generates a fresh random IV for every encryption operation and stores it alongside the ciphertext. Never reuse an IV.
PBKDF2-SHA256: Password-Based Key Derivation
PBKDF2 transforms a user-chosen passphrase into a 256-bit AES key suitable for encryption. It is used exclusively for Phantom Identity passphrase protection.
Parameters
| Parameter | Value |
|---|---|
| Hash function | SHA-256 |
| Iterations | 100,000 |
| Salt | 16 bytes (random, stored in PNG metadata) |
| Output length | 256 bits (32 bytes) |
| Implementation | Browser WebCrypto API (crypto.subtle.deriveKey) |
Why 100,000 Iterations?
The iteration count is a trade-off between user experience (derivation time) and brute-force resistance. At 100,000 iterations:
| Attacker Capability | Time to Crack (40-bit passphrase entropy) | Time to Crack (80-bit passphrase entropy) |
|---|---|---|
| Single CPU core | ~35 years | ~ years |
| GPU cluster (10,000 cores) | ~1.3 days | ~ years |
| ASIC farm | Hours | ~ years |
For high-value identities, users should choose passphrases with at least 80 bits of entropy (approximately 6+ random words from a large dictionary). The iteration count provides a speed bump, not a wall — passphrase strength is the primary defense.
Salt Purpose
The random salt ensures that identical passphrases produce different AES keys. Without a salt, an attacker could precompute a rainbow table mapping common passphrases to AES keys. With a 16-byte random salt, each PNG has a unique derivation path, forcing the attacker to brute-force each PNG independently.
HKDF-SHA256: Seed-Based Key Derivation
HKDF (HMAC-based Key Derivation Function) deterministically derives multiple independent secrets from a single 128-bit seed. It is the core derivation function for Phantom Keys.
Derivation Table
| Output | HKDF Info String | Output Size | Reduction |
|---|---|---|---|
secret | ghostcoin-secret-v1 | 32 bytes | mod BN254 field prime |
nullifierSecret | ghostcoin-nullifier-v1 | 32 bytes | mod BN254 field prime |
blinding | ghostcoin-blinding-v1 | 32 bytes | mod BN254 field prime |
changeBlinding | ghostcoin-change-blinding-v1 | 32 bytes | mod BN254 field prime |
quantumSecret | ghostcoin-quantum-v1 | 32 bytes | None (raw 256-bit value) |
Why HKDF?
HKDF is a standardized KDF (RFC 5869) with formal security proofs. Each derivation uses a unique info string, which acts as a domain separator — the outputs for different info strings are cryptographically independent even from the same seed. This means compromising secret reveals nothing about nullifierSecret, and vice versa.
BN254 Field Reduction
HKDF outputs 32 bytes (256 bits), but BN254 field elements must be less than the scalar field prime (). The 256-bit output is reduced modulo . Because the output is approximately uniformly distributed over 256 bits, the bias introduced by modular reduction is negligible ().
Split-Key XOR Architecture
The split-key architecture protects the secp256k1 private key in Phantom Identity by splitting it across two storage locations.
Construction
encKeyPartA = random(256 bits) // stored in PNG
encKeyPartB = privateKey XOR encKeyPartA // stored on-chain
Recovery
privateKey = encKeyPartA XOR encKeyPartB
Security Analysis
| Property | Guarantee |
|---|---|
| Information-theoretic security | When encKeyPartA is uniformly random, encKeyPartB is also uniformly random regardless of privateKey. Neither part alone leaks any information. |
| No computational assumptions | XOR security does not depend on the hardness of any mathematical problem. It is unconditionally secure for the split — no quantum computer or future algorithm can break it. |
| Both parts required | Recovery requires both parts. An attacker with only one part has zero bits of information about the private key. |
The XOR split is the simplest possible secret sharing scheme. It is a degenerate case of a 2-of-2 secret sharing scheme where the shares are exactly the same size as the secret. The simplicity eliminates implementation risk — there are no parameters to misconfigure, no threshold to set, and no reconstruction algorithm beyond a single XOR.
Quantum-Resistant keccak256 Commitments
The quantum commitment layer provides defense against a future adversary with a quantum computer capable of breaking the BN254 pairing (and thus forging Groth16 proofs).
Mechanism
| Step | Operation | When |
|---|---|---|
| Initial | Identity creation (seal) | |
| Access | Verify: | Each connect |
| Rotation | After verification |
Why keccak256?
keccak256 (SHA-3 family) is a hash function based on the sponge construction. Its preimage resistance relies on the security of the Keccak permutation, which is unrelated to the discrete logarithm problem or elliptic curve pairings. Grover's algorithm provides at most a quadratic speedup for hash preimage search, reducing 256-bit security to approximately 128-bit equivalent — still computationally infeasible.
| Threat | BN254 (Groth16) | keccak256 |
|---|---|---|
| Classical computer | Secure (~128 bits) | Secure (~256 bits) |
| Quantum computer (Shor) | Broken (polynomial time for discrete log) | Not applicable (no algebraic structure) |
| Quantum computer (Grover) | N/A | Reduced to ~128 bits (still secure) |
Rotation Benefits
The rotation ensures that each quantum secret is single-use. After a successful access:
- The previous
quantumSecretis no longer valid (the commitment has been updated). - Only the current holder knows the next valid preimage (they chose
newQuantumCommitment). - An attacker who records all on-chain quantum commitments learns nothing — they are all keccak256 outputs, and finding the preimage requires breaking keccak256.
Key Binding: keyId Derivation
The keyId that addresses entries in the PersistentKeyVault is derived as:
Why Include issuerAddress?
Including the issuer's Ethereum address in the key ID prevents front-running attacks during the seal phase:
| Attack | Without issuerAddress | With issuerAddress |
|---|---|---|
| Attacker sees commitment in mempool | Attacker races to store their own encKeyPartB at keyId = keccak256(commitment) | Attacker cannot match the keyId because keccak256(commitment, attackerAddress) != keccak256(commitment, issuerAddress) |
| Result | Attacker's key part B is stored; real user's is rejected | Attacker's entry is at a different keyId; no collision |
The issuer address is the msg.sender of the storeKeyPartB transaction. The vault enforces that the caller's address matches the one used in the keyId derivation. This binds the key storage to the identity creator and prevents anyone else from claiming the same key slot.
Client-Side Execution Model
All cryptographic operations that handle plaintext data or private keys execute in the user's browser or local environment. The protocol's on-chain components never see:
| Never On-Chain | Where It Lives |
|---|---|
| Plaintext data | Client memory only |
| Private keys (secp256k1) | Client memory during session, split-encrypted at rest |
| Commitment secrets (secret, nullifierSecret, blinding) | PNG metadata (optionally encrypted) or derived from numeric key seed |
| Passphrases | Client memory during decryption, never stored |
| quantumSecret (current preimage) | Client memory during access, PNG metadata at rest |
| Groth16 witness | Client memory during proof generation |
The chain stores:
- Commitments: Poseidon hashes (opaque 254-bit values).
- Nullifiers: Poseidon hashes (opaque, unlinkable to commitments).
- Access tags: Poseidon hashes (opaque, session-scoped).
- Encrypted key parts:
encKeyPartB(useless without the correspondingencKeyPartA). - Quantum commitments: keccak256 outputs (opaque without preimage).
- Proofs: Groth16 proof data (, , ) — verifiable but zero-knowledge.
This architecture means that even a fully compromised chain — validators, RPC nodes, block explorers, everything — reveals nothing about users' data. The on-chain state is a collection of opaque hashes, encrypted fragments, and zero-knowledge proofs. The plaintext exists only in the client's memory, for only as long as the client needs it.