NERO
Security & Recovery

Self-Custody Recovery

Offline private key reconstruction using password-protected composite blobs

Self-Custody Recovery

The self-custody recovery system addresses a critical scenario: when the NERO backend becomes permanently unavailable, users holding only their client share (sk_A) cannot sign transactions. This subsystem enables offline private key reconstruction using a password-protected composite blob, allowing users to recover funds without network access or server trust.

Threat Model

In normal DKLS operation, the client holds sk_A and the backend holds sk_B, with the full private key never assembled. Device loss recovery works via extractClientShare() when NERO is online. Permanent backend failure requires offlineReconstructKey() to combine both shares offline.

ScenarioFunctionNetworkFull KeyOutput
Device lost, NERO onlineextractClientShare()RequiredNosk_A only; MPC resumes
NERO permanently offlineofflineReconstructKey()Not requiredYessk = sk_A * sk_B (mod q)

Composite Blob Format

The setup process produces a JSON object containing encrypted shares, KDF parameters, and integrity metadata:

FieldTypePurpose
version2Schema version identifier
encryptedClientShare{ciphertext, nonce, tag}AES-256-GCM encrypted sk_A
backendShareBlob{ephemeralPublicKey, ciphertext, nonce, tag}ECDH-encrypted sk_B
sharingType"multiplicative" or "additive"Share combination method
kdfSalthex stringRandom 32-byte scrypt salt
kdfParams{N, r, p}Scrypt parameters for deterministic re-derivation
metadataMachex stringHMAC-SHA-256 over plaintext metadata fields

Cryptographic Design

Key Derivation Tree

Password -> scrypt (N=131072, r=8, p=1) -> 32-byte seed -> HKDF branches:
  ├── AES-GCM key for sk_A encryption (info: "nero-mpc:self-custody:client-share")
  ├── HMAC key for metadata MAC     (info: "nero-mpc:self-custody:metadata-mac")
  └── Factor credential for API auth (info: "nero-mpc:self-custody:factor-credential")

Key Reconstruction

After decrypting both shares, the formula depends on the sharingType field stored in the blob:

  • Multiplicative: sk = sk_A * sk_B (mod q)
  • Additive: sk = 2 * sk_A - sk_B (mod q)

Metadata Integrity

macKey = HKDF-SHA256(seed, info="nero-mpc:self-custody:metadata-mac")
payload = "<version>:<sharingType>:<kdfSalt>:<N>:<r>:<p>"
metadataMac = HMAC-SHA256(macKey, payload)

Setup Flow

setupSelfCustodyRecovery() coordinates the following steps:

  1. Validate password length (minimum 12 characters)
  2. Generate random KDF salt and derive seed via scrypt
  3. Fetch ECDH-encrypted sk_B blob via apiClient.getKeyMaterial()
  4. Build composite blob containing encrypted sk_A and sk_B
  5. Compute metadata HMAC for integrity protection
  6. Upload blob to factor API using derived credential
  7. Zero seed from memory in finally block

Offline Recovery Flow

offlineReconstructKey() operates without network access:

  1. Parse and validate composite blob structure
  2. Derive seed from password and stored KDF parameters
  3. Verify metadata HMAC against plaintext fields
  4. Decrypt encryptedClientShare using HKDF-derived AES key
  5. Decrypt backendShareBlob using ECDH and share exchange decryption
  6. Combine shares using stored sharingType formula
  7. Derive Ethereum address from reconstructed key
  8. Optionally verify against expectedAddress
  9. Return {privateKey: "0x...", walletAddress}
  10. Zero seed in finally block

Input Validation

parseCompositeBlob() validates:

  • JSON structure and required fields presence
  • Version must equal 2
  • Nonce exactly 12 bytes, tag exactly 16 bytes
  • All hex fields are valid hexadecimal
  • ephemeralPublicKey exactly 33 bytes (compressed secp256k1)
  • sharingType is "multiplicative" or "additive"
  • KDF parameters within safe bounds (N: power of 2 in [1024, 2^20], r: [1, 64], p: [1, 16])

Function Reference

FunctionPurpose
deriveRecoverySeed(password, salt, params?)Calls scryptAsync with dkLen: 32
seedToScalar(seed)Reduces 32-byte seed to bigint modulo secp256k1 curve order
seedToPublicKey(seed)Returns compressed 33-byte secp256k1 public key
buildCompositeBlob(...)Encrypts clientShareHex, assembles blob, appends metadataMac
parseCompositeBlob(json)Deserializes and validates composite blob JSON string
setupSelfCustodyRecovery(...)Full setup path requiring password length >= 12
extractClientShare(compositeJson, password)Decrypts only sk_A from composite blob
offlineReconstructKey(...)Full offline reconstruction of private key

Security Properties

PropertySetupOffline Recovery
Full private key in memoryNeverOnly at reconstruction point
Password in memoryBriefly (scrypt input)Briefly (scrypt input)
sk_A in memoryYes (then AES-GCM encrypted)Yes (after AES-GCM decrypt)
sk_B in memoryNever (ECDH blob unmodified)Yes (after ECDH decrypt)
Network requiredYesNo
Metadata tampering detectableYes (HMAC verified at parse)Yes (HMAC verified before decrypt)

Cryptographic Parameter Summary

ParameterValue
KDFscrypt (@noble/hashes)
scrypt N131072 (2^17) default
scrypt r8 default
scrypt p1 default
scrypt output32 bytes
sk_A encryptionAES-256-GCM, 12-byte nonce, 128-bit tag
sk_A key derivationHKDF-SHA-256
sk_B encryptionECDH (secp256k1) + HKDF-SHA-256 + AES-256-GCM
Metadata integrityHMAC-SHA-256
Curvesecp256k1
Minimum password length12 characters

On this page