Skip to main content

PersistentKeyVault

The PersistentKeyVault provides on-chain encrypted key storage with privacy-preserving access control. Key holders can store encrypted key material that can be accessed via ZK proofs without revealing the accessor's identity, and optionally revoked under configurable policies. This contract supports Dead Man's Switch (DMS) key recovery and inheritance workflows.

Enums

RevokePolicy

enum RevokePolicy {
BEARER, // 0 - Anyone with the nullifier can revoke
ISSUER_ONLY // 1 - Only the original key storer can revoke
}
ValueMeaning
BEARER (0)Any party that can produce a valid nullifier for the key can revoke it. Used for bearer-instrument-style keys.
ISSUER_ONLY (1)Only the address that originally called storeKeyPartB can revoke the key. Used when the issuer retains sole revocation authority.

Structs

KeyEntry

struct KeyEntry {
bytes32 commitment;
bytes encKeyPartB;
address issuer;
RevokePolicy revokePolicy;
bool revoked;
uint256 storedAt;
}
FieldTypeDescription
commitmentbytes32The Pedersen commitment associated with this key entry. Links to the Merkle tree.
encKeyPartBbytesThe encrypted key material (Part B). Encrypted client-side before storage.
issueraddressThe address that stored the key entry.
revokePolicyRevokePolicyDetermines who can revoke this key.
revokedboolWhether the key has been revoked.
storedAtuint256Block timestamp when the key was stored.

Storage

usedAccessTags

mapping(bytes32 => bool) public usedAccessTags;

Maps access tags to their usage status. Each access tag can only be used once, preventing replay of access proofs. An access tag is a unique identifier derived from the accessor's session to ensure one-time retrieval.

Functions

storeKeyPartB

function storeKeyPartB(
bytes32 keyId,
bytes32 commitment,
bytes calldata encKeyPartB,
RevokePolicy revokePolicy
) external

Stores encrypted key material on-chain.

Parameters:

NameTypeDescription
keyIdbytes32Unique identifier for this key entry. Typically a hash of key metadata.
commitmentbytes32Pedersen commitment that anchors this key in the Merkle tree.
encKeyPartBbytesEncrypted key material. Encrypted off-chain; the contract stores it opaquely.
revokePolicyRevokePolicyThe revocation policy for this key (BEARER or ISSUER_ONLY).

Behavior:

  1. Validates keyId is not already in use.
  2. Creates a KeyEntry struct with the provided parameters.
  3. Sets issuer = msg.sender and storedAt = block.timestamp.
  4. Stores the entry in the keys mapping.

Reverts if:

  • keyId is already occupied.
  • commitment is zero.
  • encKeyPartB is empty.

accessKeyPartB

function accessKeyPartB(
bytes32 keyId,
bytes calldata proof,
bytes32 root,
bytes32 dataHash,
bytes32 sessionNonce,
bytes32 accessTag
) external returns (bytes memory encKeyPartB)

Retrieves encrypted key material by providing a valid ZK proof of authorization.

Parameters:

NameTypeDescription
keyIdbytes32The key entry to access.
proofbytesGroth16 ZK proof demonstrating the caller is authorized to access this key.
rootbytes32Merkle root used in the proof. Must be a known root in CommitmentTree.
dataHashbytes32Hash of the data being accessed, included as a public input to prevent proof reuse across different keys.
sessionNoncebytes32A nonce binding the proof to a specific session, preventing replay.
accessTagbytes32A unique tag derived from the proof that is marked as used after access.

Returns:

NameTypeDescription
encKeyPartBbytesThe encrypted key material stored for this keyId.

Behavior:

  1. Validates the key exists and is not revoked.
  2. Verifies root via CommitmentTree.isKnownRoot(root).
  3. Verifies accessTag has not been used: !usedAccessTags[accessTag].
  4. Verifies the ZK proof via GhostRedemptionVerifier.
  5. Marks usedAccessTags[accessTag] = true.
  6. Returns the stored encKeyPartB.

Reverts if:

  • Key does not exist or is revoked.
  • Root is not known.
  • Access tag already used.
  • ZK proof is invalid.

revokeKey

function revokeKey(bytes32 keyId, bytes32 nullifier) external

Revokes a stored key entry, making it permanently inaccessible.

Parameters:

NameTypeDescription
keyIdbytes32The key entry to revoke.
nullifierbytes32The nullifier proving authorization to revoke.

Behavior:

Depending on the key's revokePolicy:

  • BEARER: Validates the nullifier against the NullifierRegistry and revokes.
  • ISSUER_ONLY: Requires msg.sender == keyEntry.issuer (nullifier may still be checked for record-keeping).

Sets keyEntry.revoked = true.

Reverts if:

  • Key does not exist.
  • Key is already revoked.
  • Caller is not authorized per the revokePolicy.

getKeyInfo

function getKeyInfo(bytes32 keyId) external view returns (
bytes32 commitment,
address issuer,
RevokePolicy revokePolicy,
bool revoked,
uint256 storedAt
)

Returns metadata about a key entry (excluding the encrypted key material).

Parameters:

NameTypeDescription
keyIdbytes32The key entry to query.

Returns:

NameTypeDescription
commitmentbytes32The associated commitment.
issueraddressThe address that stored the key.
revokePolicyRevokePolicyThe revocation policy.
revokedboolWhether the key is revoked.
storedAtuint256Storage timestamp.

isKeyAvailable

function isKeyAvailable(bytes32 keyId) external view returns (bool)

Checks whether a key entry exists and is not revoked.

Parameters:

NameTypeDescription
keyIdbytes32The key entry to check.

Returns:

NameTypeDescription
(unnamed)booltrue if the key exists and has not been revoked.

isAccessTagUsed

function isAccessTagUsed(bytes32 accessTag) external view returns (bool)

Checks whether an access tag has been consumed.

Parameters:

NameTypeDescription
accessTagbytes32The access tag to check.

Returns:

NameTypeDescription
(unnamed)booltrue if the tag has been used, false otherwise.

getStats

function getStats() external view returns (
uint256 totalKeys,
uint256 activeKeys,
uint256 revokedKeys,
uint256 totalAccesses
)

Returns aggregate statistics about the vault.

Returns:

NameTypeDescription
totalKeysuint256Total number of key entries ever stored.
activeKeysuint256Number of keys that are not revoked.
revokedKeysuint256Number of revoked keys.
totalAccessesuint256Total number of successful accessKeyPartB calls.

Usage Examples

Storing a Key for DMS Recovery

// Off-chain: encrypt key Part B with the beneficiary's public key
bytes memory encKeyPartB = encryptForBeneficiary(keyPartB, beneficiaryPubKey);

// Compute a unique key ID
bytes32 keyId = keccak256(abi.encodePacked("dms-recovery", userId, nonce));

// Store on-chain
PersistentKeyVault(vaultAddress).storeKeyPartB(
keyId,
commitment, // links to the Merkle tree
encKeyPartB,
PersistentKeyVault.RevokePolicy.ISSUER_ONLY
);

Accessing a Key with a ZK Proof

// Generate proof off-chain demonstrating authorization
// proof, root, dataHash, sessionNonce, accessTag are produced by the prover

bytes memory encryptedKey = PersistentKeyVault(vaultAddress).accessKeyPartB(
keyId,
proof,
root,
dataHash,
sessionNonce,
accessTag
);

// Off-chain: decrypt encryptedKey with the beneficiary's private key
bytes memory keyPartB = decrypt(encryptedKey, beneficiaryPrivKey);

Checking Key Availability

bool available = PersistentKeyVault(vaultAddress).isKeyAvailable(keyId);
if (available) {
// Key exists and can be accessed
}

// Check if a specific access has already occurred
bool used = PersistentKeyVault(vaultAddress).isAccessTagUsed(accessTag);

Revoking a Key (Issuer Only)

// Only the original storer can revoke when RevokePolicy is ISSUER_ONLY
PersistentKeyVault(vaultAddress).revokeKey(keyId, nullifier);

Security Considerations

  • Encrypted storage: The contract stores encKeyPartB opaquely. It never decrypts or interprets the key material. All encryption and decryption happens client-side.
  • One-time access tags: Each accessTag can only be used once. This prevents replay attacks where a proof is resubmitted to retrieve the key material multiple times from different sessions.
  • ZK-proof authorization: Accessors prove their authorization via ZK proofs without revealing their identity on-chain. The proof circuit validates membership in the commitment tree.
  • Revocation finality: Once a key is revoked, it cannot be un-revoked. The encrypted data remains on-chain (in event logs/storage) but accessKeyPartB will revert for revoked keys.
  • Two-part key design: The key is split into Part A (held by the user) and Part B (stored on-chain encrypted). Neither part alone is sufficient to reconstruct the full key, providing defense in depth.