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.
| Scenario | Function | Network | Full Key | Output |
|---|---|---|---|---|
| Device lost, NERO online | extractClientShare() | Required | No | sk_A only; MPC resumes |
| NERO permanently offline | offlineReconstructKey() | Not required | Yes | sk = sk_A * sk_B (mod q) |
Composite Blob Format
The setup process produces a JSON object containing encrypted shares, KDF parameters, and integrity metadata:
| Field | Type | Purpose |
|---|---|---|
version | 2 | Schema 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 |
kdfSalt | hex string | Random 32-byte scrypt salt |
kdfParams | {N, r, p} | Scrypt parameters for deterministic re-derivation |
metadataMac | hex string | HMAC-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:
- Validate password length (minimum 12 characters)
- Generate random KDF salt and derive seed via scrypt
- Fetch ECDH-encrypted
sk_Bblob viaapiClient.getKeyMaterial() - Build composite blob containing encrypted
sk_Aandsk_B - Compute metadata HMAC for integrity protection
- Upload blob to factor API using derived credential
- Zero seed from memory in finally block
Offline Recovery Flow
offlineReconstructKey() operates without network access:
- Parse and validate composite blob structure
- Derive seed from password and stored KDF parameters
- Verify metadata HMAC against plaintext fields
- Decrypt
encryptedClientShareusing HKDF-derived AES key - Decrypt
backendShareBlobusing ECDH and share exchange decryption - Combine shares using stored
sharingTypeformula - Derive Ethereum address from reconstructed key
- Optionally verify against
expectedAddress - Return
{privateKey: "0x...", walletAddress} - 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
ephemeralPublicKeyexactly 33 bytes (compressed secp256k1)sharingTypeis"multiplicative"or"additive"- KDF parameters within safe bounds (N: power of 2 in [1024, 2^20], r: [1, 64], p: [1, 16])
Function Reference
| Function | Purpose |
|---|---|
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
| Property | Setup | Offline Recovery |
|---|---|---|
| Full private key in memory | Never | Only at reconstruction point |
| Password in memory | Briefly (scrypt input) | Briefly (scrypt input) |
sk_A in memory | Yes (then AES-GCM encrypted) | Yes (after AES-GCM decrypt) |
sk_B in memory | Never (ECDH blob unmodified) | Yes (after ECDH decrypt) |
| Network required | Yes | No |
| Metadata tampering detectable | Yes (HMAC verified at parse) | Yes (HMAC verified before decrypt) |
Cryptographic Parameter Summary
| Parameter | Value |
|---|---|
| KDF | scrypt (@noble/hashes) |
| scrypt N | 131072 (2^17) default |
| scrypt r | 8 default |
| scrypt p | 1 default |
| scrypt output | 32 bytes |
sk_A encryption | AES-256-GCM, 12-byte nonce, 128-bit tag |
sk_A key derivation | HKDF-SHA-256 |
sk_B encryption | ECDH (secp256k1) + HKDF-SHA-256 + AES-256-GCM |
| Metadata integrity | HMAC-SHA-256 |
| Curve | secp256k1 |
| Minimum password length | 12 characters |