NERO
Security & Recovery

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:

  • ClientKeyManager manages encrypted key shares for the Pedersen DKG path
  • DKLS path writes key material directly through a StorageAdapter interface, independent of ClientKeyManager

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

FieldTypeDefaultDescription
storagePrefixstring (optional)"nero"Namespace prefix for underlying storage

Constructor Behavior

  1. Calls createSecureStorage(deviceKey, storagePrefix) to produce a SecureKeyStorage instance
  2. Uses deviceKey as the AES-GCM encryption key for all stored data (the device key itself is never persisted)
  3. Initializes currentUserId, cachedKeyShare, and cachedPartyShares to null

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.

MethodGuard ConditionSide Effect
hasKeyShare()Returns false if not initializedDelegates to storage.hasKeyShare(userId)
getKeyShare()Returns null if not initializedPopulates cachedKeyShare on miss
storeKeyShare(keyShare)Throws if no user initializedWrites to storage and updates cache
deleteKeyShare()No-op if not initializedRemoves from storage and nulls cache
rotateKeyShare(newKeyShare)Throws if no share exists or party IDs mismatchDelegates 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.

MethodBehavior
getPartyPublicShares()Returns cache if available; loads from storage otherwise
storePartyPublicShares(shares)Writes to storage and updates cachedPartyShares

Public Key and Backend Share

MethodData 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 string
  • deriveDeviceKey(userAgent, userId, additionalEntropy?): Returns SHA-256 hash of concatenated inputs; useful for deterministic key reproduction across page reloads

Device Key Loading Sequence

  1. Attempt to retrieve from IndexedDB
  2. Check for legacy key in localStorage
  3. Migrate if found
  4. Generate new cryptographically random key if not found
  5. 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):

  1. Converts KeyShare to JSON string
  2. Encrypts using the device key
  3. Wraps in EncryptedKeyShare object with versioning
  4. Persists using hashed key: keyshare:${sha256(userId)}

getKeyShare(userId):

  1. Retrieves and parses EncryptedKeyShare from storage
  2. Validates version field (currently version 1)
  3. Decrypts ciphertext using device key
  4. Returns reconstructed KeyShare object

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

AdapterPersistenceAPIUse Case
IndexedDBStoragePermanentindexedDBLarge encrypted key shares (preferred)
LocalStorageAdapterPermanentlocalStorageAuth tokens, config items
SessionStorageAdapterTab-onlysessionStorageTemporary session state
MemoryStorageEphemeralJavaScript MapTesting, 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>

  1. Retrieves the current key share; throws NO_KEY_SHARE if absent
  2. Encrypts using encryptWithPassword(JSON.stringify(keyShare), password)
  3. Wraps the result in an object with version: 1 and type: "nero-mpc-backup"
  4. Returns btoa(JSON.stringify(backup))

importBackup(backupString: string, password: string): Promise<KeyShare>

  1. Decodes with atob and parses JSON
  2. Validates backup.type === "nero-mpc-backup"; throws INVALID_BACKUP on mismatch
  3. Validates backup.version === 1; throws UNSUPPORTED_VERSION otherwise
  4. Decrypts with decryptWithPassword(backup.data, password)
  5. Returns the parsed KeyShare — does not persist automatically (the caller is responsible for calling storeKeyShare)

Address Derivation

deriveEOAAddress(publicKey: string): string

  1. Accepts uncompressed or raw hex format (128 or 130 characters)
  2. Takes the last 64 bytes of the uncompressed key
  3. Applies keccak256 hash (from @noble/hashes/sha3)
  4. Extracts the rightmost 20 bytes
  5. Returns a checksummed address (EIP-55)

Error Codes

CodeMethodCondition
USER_NOT_INITIALIZEDStorage write methodscurrentUserId is null
NO_KEY_SHARErotateKeyShare, exportBackupNo key share exists
PARTY_ID_MISMATCHrotateKeyShareIncoming party ID differs from existing
INVALID_BACKUPimportBackupbackup.type is not "nero-mpc-backup"
UNSUPPORTED_VERSIONimportBackupbackup.version is not 1
INVALID_PUBLIC_KEYderiveEOAAddressPublic key hex length not 128 or 130 chars

Key Design Decisions

  1. Dual-layer approach: Encryption happens at the SecureKeyStorage level, not at the adapter level
  2. Hashed user IDs: Sensitive identifiers are SHA-256 hashed before use as storage keys
  3. Version pinning: Encrypted shares include version metadata for backward compatibility
  4. Safe clear() implementation: Prevents data loss for shared storage origins
  5. Fallback chain: IndexedDB -> localStorage -> Memory ensures availability in all environments

On this page