Key Management
How the NERO MPC SDK stores, encrypts, backs up, and recovers key shares across storage adapters
Key Management
The NERO MPC SDK uses a layered storage architecture to securely persist key shares and session data on the client side. Two distinct systems handle different protocol paths:
ClientKeyManagermanages encrypted key shares for the Pedersen DKG path- DKLS path writes key material directly through a
StorageAdapterinterface, independent ofClientKeyManager
ClientKeyManager
File: src/core/client-key-manager.ts
The ClientKeyManager wraps a SecureKeyStorage instance and provides typed methods for managing private key shares, party public shares, the raw public key, and cached backend shares.
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
storagePrefix | string (optional) | "nero" | Namespace prefix for underlying storage |
Constructor Behavior
- Calls
createSecureStorage(deviceKey, storagePrefix)to produce aSecureKeyStorageinstance - Uses
deviceKeyas the AES-GCM encryption key for all stored data (the device key itself is never persisted) - Initializes
currentUserId,cachedKeyShare, andcachedPartySharestonull
Lifecycle Methods
initialize(userId: string): Promise<void> — Sets the current user ID and eagerly loads that user's key share from storage into the in-memory cache. Called post-login on the NeroMpcSDK instance.
clear(): Promise<void> — Calls storage.clearAll() to remove all persisted data, then resets currentUserId, cachedKeyShare, and cachedPartyShares to null. Invoked during logout or device rotation.
Key Share Operations
ClientKeyManager maintains a single-slot cache (cachedKeyShare) per user session. Cache hits return immediately without storage I/O; misses populate the cache on first access.
| Method | Guard Condition | Side Effect |
|---|---|---|
hasKeyShare() | Returns false if not initialized | Delegates to storage.hasKeyShare(userId) |
getKeyShare() | Returns null if not initialized | Populates cachedKeyShare on miss |
storeKeyShare(keyShare) | Throws if no user initialized | Writes to storage and updates cache |
deleteKeyShare() | No-op if not initialized | Removes from storage and nulls cache |
rotateKeyShare(newKeyShare) | Throws if no share exists or party IDs mismatch | Delegates to storeKeyShare after validation |
rotateKeyShare() enforces that incoming shares have the same partyId as existing ones.
Party Public Shares
Stored as Map<number, string> where keys are partyId values and values are coefficient commitments (compressed public key points). Used by SigningClient during Pedersen signing to verify partial signatures.
| Method | Behavior |
|---|---|
getPartyPublicShares() | Returns cache if available; loads from storage otherwise |
storePartyPublicShares(shares) | Writes to storage and updates cachedPartyShares |
Public Key and Backend Share
| Method | Data Stored |
|---|---|
storePublicKey(publicKey) | Joint public key from DKG completion |
getPublicKey() | Retrieves stored public key |
storeBackendShare(share) | Backend's key share representation |
getBackendShare() | Retrieves backend share |
The backend share is stored locally to enable self-custody recovery operations without network calls.
Device Key
A unique device key is generated per browser instance using the Web Crypto API. This key encrypts all sensitive material before storage.
Device Key Utilities
generateDeviceKey(): Returns a 32-byte cryptographically random hex stringderiveDeviceKey(userAgent, userId, additionalEntropy?): Returns SHA-256 hash of concatenated inputs; useful for deterministic key reproduction across page reloads
Device Key Loading Sequence
- Attempt to retrieve from IndexedDB
- Check for legacy key in localStorage
- Migrate if found
- Generate new cryptographically random key if not found
- Store using available mechanism (IndexedDB preferred, localStorage fallback)
Storage Architecture
SecureKeyStorage
Handles encryption/decryption using a device key before delegating to underlying storage adapters. All encryption uses AES-GCM via encryptWithPassword.
storeKeyShare(userId, keyShare):
- Converts
KeyShareto JSON string - Encrypts using the device key
- Wraps in
EncryptedKeyShareobject with versioning - Persists using hashed key:
keyshare:${sha256(userId)}
getKeyShare(userId):
- Retrieves and parses
EncryptedKeySharefrom storage - Validates version field (currently version 1)
- Decrypts ciphertext using device key
- Returns reconstructed
KeyShareobject
Data Persistence Flow
KeyShare (object)
│
JSON.stringify()
│
encryptWithPassword(deviceKey) -> AES-GCM ciphertext
│
EncryptedKeyShare { version, ciphertext, nonce }
│
StorageAdapter.set(key, stringified)
│
Physical Storage (IndexedDB/localStorage/Memory)Storage Adapters
The SDK supports pluggable storage backends through the StorageAdapter interface:
interface StorageAdapter {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
}Available Implementations
| Adapter | Persistence | API | Use Case |
|---|---|---|---|
IndexedDBStorage | Permanent | indexedDB | Large encrypted key shares (preferred) |
LocalStorageAdapter | Permanent | localStorage | Auth tokens, config items |
SessionStorageAdapter | Tab-only | sessionStorage | Temporary session state |
MemoryStorage | Ephemeral | JavaScript Map | Testing, SSR fallback |
IndexedDBStorage
The preferred storage backend, supporting larger data volumes than localStorage.
- Database:
nero-mpc-sdk - Object Store:
encrypted-data - Key Path: Uses prefix (default:
"nero") to avoid collisions
LocalStorageAdapter and SessionStorageAdapter
LocalStorageAdapter persists data across browser sessions. SessionStorageAdapter clears data when the page session ends (tab closed). Both implement clear() as a console warning no-op to prevent accidental deletion of non-SDK data sharing the same origin.
MemoryStorage
Non-persistent fallback using a JavaScript Map for environments where browser storage APIs are unavailable (SSR contexts, private browsing modes).
Factory Function
createTokenStorage() returns a LocalStorageAdapter if available, otherwise falls back to MemoryStorage. Used for storing JWTs and session metadata.
Fallback Chain
IndexedDB -> localStorage -> Memory ensures availability in all environments.
Backup and Recovery
exportBackup(password: string): Promise<string>
- Retrieves the current key share; throws
NO_KEY_SHAREif absent - Encrypts using
encryptWithPassword(JSON.stringify(keyShare), password) - Wraps the result in an object with
version: 1andtype: "nero-mpc-backup" - Returns
btoa(JSON.stringify(backup))
importBackup(backupString: string, password: string): Promise<KeyShare>
- Decodes with
atoband parses JSON - Validates
backup.type === "nero-mpc-backup"; throwsINVALID_BACKUPon mismatch - Validates
backup.version === 1; throwsUNSUPPORTED_VERSIONotherwise - Decrypts with
decryptWithPassword(backup.data, password) - Returns the parsed
KeyShare— does not persist automatically (the caller is responsible for callingstoreKeyShare)
Address Derivation
deriveEOAAddress(publicKey: string): string
- Accepts uncompressed or raw hex format (128 or 130 characters)
- Takes the last 64 bytes of the uncompressed key
- Applies
keccak256hash (from@noble/hashes/sha3) - Extracts the rightmost 20 bytes
- Returns a checksummed address (EIP-55)
Error Codes
| Code | Method | Condition |
|---|---|---|
USER_NOT_INITIALIZED | Storage write methods | currentUserId is null |
NO_KEY_SHARE | rotateKeyShare, exportBackup | No key share exists |
PARTY_ID_MISMATCH | rotateKeyShare | Incoming party ID differs from existing |
INVALID_BACKUP | importBackup | backup.type is not "nero-mpc-backup" |
UNSUPPORTED_VERSION | importBackup | backup.version is not 1 |
INVALID_PUBLIC_KEY | deriveEOAAddress | Public key hex length not 128 or 130 chars |
Key Design Decisions
- Dual-layer approach: Encryption happens at the
SecureKeyStoragelevel, not at the adapter level - Hashed user IDs: Sensitive identifiers are SHA-256 hashed before use as storage keys
- Version pinning: Encrypted shares include version metadata for backward compatibility
- Safe
clear()implementation: Prevents data loss for shared storage origins - Fallback chain: IndexedDB -> localStorage -> Memory ensures availability in all environments