replaceForLastDeviceRecovery

Replaces the target's key by a last-device recovery (docs/last-device-recovery.md) in one atomic step, together with the consumption of its challenge (LastDeviceRecoveryRepository). No nonce is claimed: the server-issued challenge is the single-use value. In this order:

  1. the target has no registration: LastDeviceRecoveryReplacementResult.NOT_REGISTERED;

  2. the target's current key was installed by the last-device recovery with LastDeviceRecoveryReplacement.recoveryId: LastDeviceRecoveryReplacementResult.ALREADY_APPLIED, nothing written;

  3. the target's user has no active recovery key (none registered, or revoked), or another one than LastDeviceRecoveryReplacement.expectedRecoveryPublicKey: LastDeviceRecoveryReplacementResult.NOT_CONFIGURED, nothing written;

  4. the target has no stored challenge with this ID and nonce, or it was issued under another recovery key epoch than the current one (docs/recovery-key-lifecycle.md): LastDeviceRecoveryReplacementResult.CHALLENGE_INVALID; it has, but it expired before LastDeviceRecoveryReplacement.now: LastDeviceRecoveryReplacementResult.EXPIRED;

  5. prunes every challenge that expired before now;

  6. the target's key or epoch differ from the expected state or from the challenge's, or the replacement key is the current key: LastDeviceRecoveryReplacementResult.CONFLICT;

  7. the epoch is Long.MAX_VALUE: LastDeviceRecoveryReplacementResult.EPOCH_EXHAUSTED;

  8. deletes the challenge, stores the replacement key, epoch + 1, the last-device recovery ID and now as installation time, and clears the recovery and rotation IDs: LastDeviceRecoveryReplacementResult.REPLACED.

Every result except LastDeviceRecoveryReplacementResult.REPLACED leaves the registration and the challenge unchanged (pruning expired challenges may commit). The same compare-and-set as replaceForRecovery and replaceForRotation: of transitions from the same state, exactly one replaces the key. Nothing else changes: prekeys, mailboxes and nonces stay.