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:
remove the old key id from each 'm.cross_signing.*' entry's
encrypted object, leaving the new one;
PUT the entries in the order you were given them, pointer last;
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.
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.
accountDatais 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
recoveryKeycomes 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-sha2scheme, produced bymatrix-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,
PUTeach entry ofaccountDatato/_matrix/client/v3/user/{userId}/account_data/{eventType}with the entry'scontentas 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'meansaccountDatanames 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
accountDataminus 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 lastPUT, 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
encryptedmap, 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:
'm.cross_signing.*'entry'sencryptedobject, leaving the new one;PUTthe entries in the order you were given them, pointer last;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:
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.[]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.