-
Notifications
You must be signed in to change notification settings - Fork 742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fallback keys implementation #3970
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some remarks
matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
Outdated
Show resolved
Hide resolved
matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
Outdated
Show resolved
Hide resolved
matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
Outdated
Show resolved
Hide resolved
matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
Outdated
Show resolved
Hide resolved
...dk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeysUploadBody.kt
Outdated
Show resolved
Hide resolved
Also, of course you will have to remove the binary and upgrade the dependency on Olm library once this one will be release. |
c7bb75b
to
953dcea
Compare
Done |
953dcea
to
a820555
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks sensible.
It's a bit weird that we're not persisting the fact that a fallback key needs to be uploaded, but the server seems to return fallback key info always and the MSC seems to suggest that this is intentional so.
So all seems to be good.
@onurays can you add a file for the changelog please? |
return null | ||
} | ||
|
||
fun generateFallbackKey() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be called several times if uploading the keys failed for some reason. Is it OK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is ok since we have the condition on sync response. I can't see any other way to protect it in client side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was discussed in chat, though never fully answered. The one-time keys will be gone from the json output if we call mark_keys_as_published()
, so there's a boolean flag on the libolm side that helps.
Let me reintroduce the context. When you call the method to generate the keys (this is true for both one-time as well as fallback keys) you populate some limited amount of memory with keypairs (a private and public ed25519 key). The public parts of those keypairs will be shown in the output of your getFallbackKey()
method.
Now for one-time keys, if we call mark_keys_as_published()
, the public parts of the keys will be gone from the getter. This means that you can do the following flow:
- Sync tells us the new one-time key count
- If it's lower than our MAX and the output of
getOneTimeKeys()
is empty, generate some one-time keys - If there are keys in the
getOneTimeKeys()
output, upload them.
It's crucial that we don't call 2. blindly every time the server tells us so. As mentioned we store an limited amount of keypairs and if we generate new ones, the oldest ones will be replaced by the freshly generated ones.
For fallback keys the amount of keypairs we store in libolm is 2. One slot is for the current, unused, fallback key, the other one for the previous fallback key.
Now the problem is, mark_keys_as_published()
does not touch the fallback keys. It isn't quite clear how to handle this considering the above.
@uhoreg could you clarify how this is supposed to work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a more global problem, would be fixed by some changes on lib olm. Will be handle in a next PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? If you want to merge this PR before libolm is fixed you can easily store the fact that a fallback key has been uploaded or not yourself:
In [1]: import olm
In [2]: x = olm.Account()
In [3]: x.fallback_key
Out[3]: {'curve25519': {}}
In [4]: x.generate_fallback_key()
In [5]: x.fallback_key
Out[5]: {'curve25519': {'AAAAAQ': '2XcMkoLlaJrKeNxV7yH4lUlJKc87u4QSrjbs/Js2KxQ'}}
In [6]: # Store the key ID 'AAAAAQ` and a boolean if it has been uploaded or not
On the other hand, this PR has been open now for more than a month, I raised the issue 3 weeks ago without any significant movement on it until now. Waiting a day or two for the libolm fix wouldn't add a significant amount of delay here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, can wait it will be on next EA release
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR is updated, the java binding are now directly calling the new unpublished_*
olm calls for fallback keys.
So if a new fallback key is needed but there is still an unpublished one locally we retry to upload it.
Also added simple mechanism to forget fallback key if a new one as been published more than 5mn ago.
Tested against https://gitlab.matrix.org/uhoreg/matrix-client-test-bot
matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
Outdated
Show resolved
Hide resolved
matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned, we should indeed somewhere store the fact if a fallback key is uploaded or not. But let's see what the intended method here is.
4d4eee4
to
aacde9d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
3a34d96
to
c092656
Compare
Author: Onuray - Benoit squashes the 4 commit to cancel the addition on binaries
And forgetFallback after 5mn
c092656
to
5d35f02
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we can simplify this a bit. Removing the flag and moving the hasUnpublishedFallbackKey() check would simplify the code quite a bit.
Otherwise looks good.
if (syncResponse.deviceUnusedFallbackKeyTypes != null && | ||
// Generate a fallback key only if the server does not already have an unused fallback key. | ||
!syncResponse.deviceUnusedFallbackKeyTypes.contains(KEY_SIGNED_CURVE_25519_TYPE)) { | ||
oneTimeKeysUploader.setNeedsNewFallback() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we just generate the key directly here? It seems to be a bit convoluted to set the flag here and then somewhere else in the code the flag may or may not trigger the generation of the fallback key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes updated.
keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) | ||
olmDevice.generateOneTimeKeys(keysThisLoop) | ||
} | ||
if (needNewFallbackKey && !hasUnpublishedFallbackKey()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move the condition hasUnpublishedFallbackKey()
into the generateFallbackKey()
method. If the code changes around someone could call generateFallbackKey()
in another place, or this check may be forgotten.
This would lead to overrotation of the fallback key quite easily, if we instead only generate a fallback key if there isn't one already there, the chances of misuse are lowered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
// Check if we need to forget a fallback key | ||
val latestPublishedTime = getLastFallbackKeyPublishTime() | ||
if (latestPublishedTime != 0L && System.currentTimeMillis() - latestPublishedTime > FIVE_MINUTES) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably give this a bit more leeway, 5 minutes is quite the short amount of time in a federated world where your server might be down for a while.
Perhaps 1h?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, but would need to sync with js-sdk implementation
matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
Outdated
Show resolved
Hide resolved
matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
Outdated
Show resolved
Hide resolved
) { | ||
// tell if there is a OTK check in progress | ||
private var oneTimeKeyCheckInProgress = false | ||
|
||
// last OTK check timestamp | ||
private var lastOneTimeKeyCheck: Long = 0 | ||
private var oneTimeKeyCount: Int? = null | ||
private var needNewFallbackKey: Boolean = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This flag is now always false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is, but the spelling issues up above aren't addressed yet. I think we can merge after those are addressed as well.
…al/crypto/OneTimeKeysUploader.kt Co-authored-by: poljar <[email protected]>
…al/crypto/OneTimeKeysUploader.kt Co-authored-by: poljar <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor remarks. Having a store could be nice though, to have unit test.
@@ -41,6 +47,9 @@ internal class OneTimeKeysUploader @Inject constructor( | |||
private var lastOneTimeKeyCheck: Long = 0 | |||
private var oneTimeKeyCount: Int? = null | |||
|
|||
// Simple storage to remember when was uploaded the last fallback key | |||
private val storage = context.getSharedPreferences("OneTimeKeysUploader_${olmDevice.deviceEd25519Key.hashCode()}", Context.MODE_PRIVATE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit dirty... A proper store should be injected in the constructor...
} else { | ||
Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") | ||
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519") | ||
} | ||
} | ||
|
||
private fun saveLastFallbackKeyPublishTime(timeMillis: Long) { | ||
storage.edit().putLong("last_fb_key_publish", timeMillis).apply() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: use edit {}
} | ||
|
||
private fun getLastFallbackKeyPublishTime(): Long { | ||
return storage.getLong("last_fb_key_publish", 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defining a const should be better (in a store...)
} | ||
|
||
internal fun hasUnpublishedFallbackKey(): Boolean { | ||
return getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty().isNotEmpty() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
orEmpty().isNotEmpty()
is a bit weird :/
Latest remarks will be handled later... or not. |
Problem
Olm uses a set of one-time keys when initializing a session between two devices: Alice uploads one-time keys to her homeserver, and Bob claims one of them to perform a Diffie-Hellman to generate a shared key. As implied by the name, a one-time key is only to be used once. However, if all of Alice's one-time keys are claimed, Bob will not be able to create a session with Alice.
Proposal
A new response parameter,
device_unused_fallback_key_types
is added to /sync. If the client wants the server to have a fallback key for a given key algorithm, but that algorithm is not listed indevice_unused_fallback_key_types
, the client will upload a new key.Here is the proposed logic to decide whether we need to generate a new fallback keys or not:
We need to use the existing
UploadKeysTask
by addingfallbackKeys
parameter to the request.