react-native-matrix-crypto
    Preparing search index...

    Function createRecovery

    • Writes this account's private signing keys into server-side storage, under a key derived from passphrase, so that a device which has lost its store can get the identity back.

      This is what makes an identity survive a reinstall. Delete the application and install it again and the store goes with it. Without a recovery, what is lost is not a cache: the private signing keys were only ever on that device, so the new installation has to be verified against another device the user still has, and every person who had verified this account has to verify it again. With one, the new installation asks for the passphrase and is the same identity it was before, and nobody else has to do anything.

      accountData is the account's existing global account data, read back from your homeserver the same way recoverIdentity takes it. It is required rather than optional because this call will not write over a recovery the account already has, and passing [] is you saying there is none.

      Say what this costs, at the moment you ask for the passphrase

      recoveryKey comes back exactly once and is never stored anywhere. If your user loses it and forgets the passphrase, the account's identity is gone: nothing on the server can open the stored keys without one of them, and this library keeps no second copy. Showing the key on a screen the user taps past is how that ends in a support request nobody can answer. Make them record it.

      The passphrase is the weak half, and this library imposes no rule on it

      The encrypted keys live on your homeserver, so the passphrase is what stands between anyone who can read this account's account data and the account's private signing keys. createRecovery('') is accepted, and so is any other passphrase: no minimum length, no strength estimate and no refusal. That is a decision rather than an omission. Any threshold this library picked would be arbitrary, would be wrong for somebody, and would sit in the one place your product cannot adjust it. You know your users and your threat model; choose a policy and apply it before calling.

      A strong recovery key does not make up for a weak passphrase. Secret storage opens on either credential, so anyone who can read this account data has to beat only the weaker of the two: thirty-two random bytes are no help while '' opens the same ciphertext. What the recovery key protects is your user's own access, not the secret's confidentiality, and that is the reason to make them record it.

      This is Matrix's own format, not one this library invented

      The account data written here is secret storage as the specification defines it, the m.secret_storage.v1.aes-hmac-sha2 scheme, produced by matrix-sdk-crypto's own implementation of it. Another Matrix client signed into the same account reads the same five events with the same passphrase or recovery key, and a recovery another client wrote is one recoverIdentity restores. That interoperability is the reason this call exists and exportSecrets does not.

      It is also why the ciphertexts are merged rather than replaced. Each m.cross_signing.* event holds a map from key id to ciphertext so that more than one key can open the same secret, and this call adds its entry to whatever you handed it instead of writing a map of one. Another client's entry under its own key id is not this library's to remove.

      Nothing here reaches the network

      This library performs no request, here or anywhere. On success, PUT each entry of accountData to /_matrix/client/v3/user/{userId}/account_data/{eventType} with the entry's content as the body, in the order they are handed back. The default-key pointer is last, and sending it out of order gives up the property described at RecoverySetup.accountData.

      Nothing is queued through takeOutgoingRequests for this, and that is deliberate: the outbound pump is a body to send and a report that it was sent, with no value coming back, and account data is a read then a write. Rather than change what a pump entry means for every other kind of request, these two calls take and return the JSON and leave the two endpoints to you. It is the same shape receiveSyncChanges already uses for the one other place this library needs something from your server.

      Refusals

      'recovery_already_exists' means accountData names a recovery already. This call will not write over one. It cannot tell your two callers apart: a user replacing their own passphrase, where the old recovery key is meant to stop working, and a product writing what it believes is a first recovery for a user who already set one up in another Matrix client, where the key that stops working is one somebody wrote down and was told to keep forever. Both arrive here as the same call.

      To add a recovery deliberately, call this again with the same accountData minus the 'm.secret_storage.default_key' entry. Filter that one entry out of the array; write nothing to your homeserver to arrange it. The refusal lifts because nothing points at a key any more, everything else is still there so the ciphertexts still merge, and the recovery the account has goes on working until your last PUT, of the new pointer, switches it over. There is no window in which your user has no working recovery, and nothing to undo if you stop halfway.

      Adding a key is not revoking one

      That route re-points the account. It does not revoke anything. When it finishes, the old key description is still on your homeserver and the old key's ciphertext is still in every encrypted map, because keeping them is what the merge is for. Anyone holding the old passphrase who can read this account's account data can still open the account's private signing keys, by reading the old key description directly instead of following the pointer. recoverIdentity will not do that, because it follows the pointer; a homeserver operator, anyone with a live access token, and any client that remembers the old key id are not obliged to.

      That is the right default and it is not what every caller wants. If your user is replacing a passphrase they no longer trust, re-pointing is not enough, and this call cannot do the rest for you: the entry it would have to drop is indistinguishable from another client's, which is the same reason it refuses in the first place. Revocation is one further act, on the array this call already handed you:

      1. remove the old key id from each 'm.cross_signing.*' entry's encrypted object, leaving the new one;
      2. PUT the entries in the order you were given them, pointer last;
      3. afterwards, and only afterwards, PUT {} to 'm.secret_storage.key.<old id>'.

      After step 1 the old key opens nothing on this account, whoever it belonged to. Do it only for a key your own product created.

      Do not clear the key description before the new pointer is live. The ordering is the whole difference between a rotation and a loss: the description holds the salt, the iteration count and the MAC, so a key whose description is gone can never be reconstructed from any secret, and clearing it while it is still the account's default leaves your user pointing at something nothing can open. Step 3 is that same write after the switchover, when the key it describes is no longer the one the account resolves.

      Two other routes lift the refusal, and both cost something the one above does not:

      • Clearing the pointer on your homeserver (PUT {}, which is how the client-server API deletes account data) works, and the merge survives. What it costs is a window: from that write until your last one the account resolves no recovery, and a crash in between leaves it there.
      • Passing [] works too, and costs the merge. This call merges into what you hand it, so handed nothing it merges into nothing and every other key's ciphertext, including another client's, is dropped from the events you then write. Use it only for an account you know has no account data.

      This call believes the account data you hand it, which is what makes all three possible. Passing [] asserts the account has no recovery and the refusal believes you, exactly as bootstrapCrossSigning's gate believes a key query you reported as answered. That is unavoidable in a library that performs no request, and it is said rather than left to be discovered: what the refusal buys is not that destruction is impossible, but that you have to have looked, and that the cheapest way past it is also the one that destroys nothing. What it does not buy, and what no argument to this call can buy, is that the key you replaced has stopped working. That is the further act above.

      'account_keys_not_fetched' means this process has not yet asked the server about this account. The private keys this device holds may belong to an identity the account has already replaced, and a recovery written for those opens perfectly and restores an identity that no longer exists, with nothing said at the time. The call queues that key query as it refuses, so the remedy is the ordinary loop: drain, send, report sent, call again.

      'private_keys_not_held' means this device does not hold all three private signing keys, so there is nothing to write. Read getIdentityStatus: an account with no identity needs bootstrapCrossSigning, and an identity this device has not joined needs requestSelfVerification. A partial write is not offered as an alternative, because account data that opens with the right passphrase and restores half an identity is worse than none.

      Parameters

      Returns Promise<RecoverySetup>