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
}
| Value | Meaning |
|---|---|
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;
}
| Field | Type | Description |
|---|---|---|
commitment | bytes32 | The Pedersen commitment associated with this key entry. Links to the Merkle tree. |
encKeyPartB | bytes | The encrypted key material (Part B). Encrypted client-side before storage. |
issuer | address | The address that stored the key entry. |
revokePolicy | RevokePolicy | Determines who can revoke this key. |
revoked | bool | Whether the key has been revoked. |
storedAt | uint256 | Block 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:
| Name | Type | Description |
|---|---|---|
keyId | bytes32 | Unique identifier for this key entry. Typically a hash of key metadata. |
commitment | bytes32 | Pedersen commitment that anchors this key in the Merkle tree. |
encKeyPartB | bytes | Encrypted key material. Encrypted off-chain; the contract stores it opaquely. |
revokePolicy | RevokePolicy | The revocation policy for this key (BEARER or ISSUER_ONLY). |
Behavior:
- Validates
keyIdis not already in use. - Creates a
KeyEntrystruct with the provided parameters. - Sets
issuer = msg.senderandstoredAt = block.timestamp. - Stores the entry in the
keysmapping.
Reverts if:
keyIdis already occupied.commitmentis zero.encKeyPartBis 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:
| Name | Type | Description |
|---|---|---|
keyId | bytes32 | The key entry to access. |
proof | bytes | Groth16 ZK proof demonstrating the caller is authorized to access this key. |
root | bytes32 | Merkle root used in the proof. Must be a known root in CommitmentTree. |
dataHash | bytes32 | Hash of the data being accessed, included as a public input to prevent proof reuse across different keys. |
sessionNonce | bytes32 | A nonce binding the proof to a specific session, preventing replay. |
accessTag | bytes32 | A unique tag derived from the proof that is marked as used after access. |
Returns:
| Name | Type | Description |
|---|---|---|
encKeyPartB | bytes | The encrypted key material stored for this keyId. |
Behavior:
- Validates the key exists and is not revoked.
- Verifies
rootviaCommitmentTree.isKnownRoot(root). - Verifies
accessTaghas not been used:!usedAccessTags[accessTag]. - Verifies the ZK proof via
GhostRedemptionVerifier. - Marks
usedAccessTags[accessTag] = true. - 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:
| Name | Type | Description |
|---|---|---|
keyId | bytes32 | The key entry to revoke. |
nullifier | bytes32 | The nullifier proving authorization to revoke. |
Behavior:
Depending on the key's revokePolicy:
- BEARER: Validates the
nullifieragainst theNullifierRegistryand 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:
| Name | Type | Description |
|---|---|---|
keyId | bytes32 | The key entry to query. |
Returns:
| Name | Type | Description |
|---|---|---|
commitment | bytes32 | The associated commitment. |
issuer | address | The address that stored the key. |
revokePolicy | RevokePolicy | The revocation policy. |
revoked | bool | Whether the key is revoked. |
storedAt | uint256 | Storage timestamp. |
isKeyAvailable
function isKeyAvailable(bytes32 keyId) external view returns (bool)
Checks whether a key entry exists and is not revoked.
Parameters:
| Name | Type | Description |
|---|---|---|
keyId | bytes32 | The key entry to check. |
Returns:
| Name | Type | Description |
|---|---|---|
| (unnamed) | bool | true 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:
| Name | Type | Description |
|---|---|---|
accessTag | bytes32 | The access tag to check. |
Returns:
| Name | Type | Description |
|---|---|---|
| (unnamed) | bool | true 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:
| Name | Type | Description |
|---|---|---|
totalKeys | uint256 | Total number of key entries ever stored. |
activeKeys | uint256 | Number of keys that are not revoked. |
revokedKeys | uint256 | Number of revoked keys. |
totalAccesses | uint256 | Total 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
encKeyPartBopaquely. It never decrypts or interprets the key material. All encryption and decryption happens client-side. - One-time access tags: Each
accessTagcan 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
accessKeyPartBwill 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.