Skip to content

platform-wallet: contact xpub decryption fails for a canonical ENCRYPTION key that dashj decrypts, and the failure permanently breaks the channel #4407

Description

@bfoss765

Summary

On a mainnet DashPay wallet with 120 contacts, one contact's encrypted xpub cannot be decrypted by platform-wallet, and the channel is marked permanently broken. The same xpub decrypts successfully in dashj on the same wallet, so the ciphertext is valid and a correct key exists.

This is not the legacy key-purpose cohort fixed by #4372 (that fix is present in the build under test). Both referenced keys are canonical.

Evidence

The drain: building external account diagnostic, immediately before the failure:

identity_index=0
our_key_id=2    our_key_purpose=Some(ENCRYPTION)   our_key_type=Some(ECDSA_SECP256K1)
their_key_id=2  their_key_purpose=Some(ENCRYPTION) their_key_type=Some(ECDSA_SECP256K1)
ciphertext_len=96
legacy_widened=false
ecdh_path=m/9'/5'/5'/0'/0'/0'/2'

and the failure ~80 ms later:

drain: external register permanent fault; marking channel broken
error=Invalid identity data: Failed to decrypt contact xpub (Decryption failed);
ciphertext 96 bytes. PKCS7 rejected the plaintext — most likely the ECDH shared
secret did not match the sender's, though a corrupted ciphertext produces the
same symptom

Everything here is canonical:

  • both keys are ENCRYPTION purpose and ECDSA_SECP256K1 type — purposes we mint ourselves
  • legacy_widened=false, so the fix(platform-wallet): accept legacy dashj key purposes on inbound contact requests #4372 receive-side widening played no part
  • ciphertext_len=96, the expected size
  • the ECDH path matches identity_auth_derivation_path_for_type's documented format
    m/9'/COIN'/5'/0'/key_derivation_type'/identity_index'/key_index' with key_index=2,
    which agrees with our_key_id=2

Why this is a platform-wallet bug, not bad data

On the same wallet, the same device and the same contact, dashj derives the friendship chains successfully — both account references resolve, the receiving chain yields addresses, and the sending path is present. There are zero decryption failures on the dashj side across the whole session.

So one implementation reads this contact's xpub and the other cannot, from identical inputs. The remaining explanation is that platform-wallet derives a different private key (or a different shared secret) for this identity key slot than dashj does.

Scope on the affected wallet: 119 of 120 contacts register fine; only this one fails.

Second defect: the failure is unappealable

register_external_contact_account classifies a decrypt failure as Permanent, which marks the channel broken with no retry:

platform_encryption::decrypt_extended_public_key(&shared_key, contact_encrypted_xpub)
    .map_err(|e| Permanent(PlatformWalletError::InvalidIdentityData(...)))?

A wrong shared secret is not proof that the document is bad — as this case shows, the same ciphertext is decryptable. Treating it as permanent means one bad key guess disables a contact for the life of the wallet, with no user-visible cause and no way to recover. The user can neither pay that contact nor match payments from them.

Suggested split:

  1. root cause — reconcile the identity-key derivation / ECDH secret against dashj for ENCRYPTION-purpose identity keys, ideally with a cross-implementation vector test
  2. independently — stop charging a decrypt failure to the document as Permanent; a shared-secret mismatch should leave the channel retryable

Reproduction

Not reproducible on demand yet — it is one contact on one field wallet. Device logs are available privately. A cross-implementation test vector (dashj-produced encrypted xpub decrypted by platform-wallet) would pin it deterministically and guard the regression.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions