Skip to content
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

Merged
merged 9 commits into from
Dec 13, 2021
Merged

Fallback keys implementation #3970

merged 9 commits into from
Dec 13, 2021

Conversation

onurays
Copy link
Contributor

@onurays onurays commented Sep 6, 2021

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 in device_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:

// The presence of device_unused_fallback_key_types indicates that the server supports fallback keys.
// If there's no unused signed_curve25519 fallback key we need a new one.
val shouldGenerateFallbackKey = 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)
} else {
    // Server does not support fallbackKey
    false
}

We need to use the existing UploadKeysTask by adding fallbackKeys parameter to the request.

Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some remarks

@bmarty
Copy link
Member

bmarty commented Sep 6, 2021

Also, of course you will have to remove the binary and upgrade the dependency on Olm library once this one will be release.
Please ensure to git squash to avoid binaries in the git history.

@onurays onurays marked this pull request as ready for review October 5, 2021 19:03
@github-actions
Copy link

github-actions bot commented Oct 5, 2021

Unit Test Results

  66 files  ±0    66 suites  ±0   50s ⏱️ ±0s
135 tests ±0  135 ✔️ ±0  0 💤 ±0  0 ±0 
418 runs  ±0  418 ✔️ ±0  0 💤 ±0  0 ±0 

Results for commit 76960f8. ± Comparison against base commit 7cf92ec.

♻️ This comment has been updated with latest results.

@bmarty bmarty force-pushed the feature/ons/fallback_keys branch from c7bb75b to 953dcea Compare October 7, 2021 10:13
@bmarty
Copy link
Member

bmarty commented Oct 7, 2021

Also, of course you will have to remove the binary and upgrade the dependency on Olm library once this one will be release. Please ensure to git squash to avoid binaries in the git history.

Done

@bmarty bmarty force-pushed the feature/ons/fallback_keys branch from 953dcea to a820555 Compare October 7, 2021 11:41
Copy link
Contributor

@poljar poljar left a 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.

@bmarty
Copy link
Member

bmarty commented Oct 11, 2021

@onurays can you add a file for the changelog please?

return null
}

fun generateFallbackKey() {
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor

@poljar poljar Oct 25, 2021

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:

  1. Sync tells us the new one-time key count
  2. If it's lower than our MAX and the output of getOneTimeKeys() is empty, generate some one-time keys
  3. 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?

Copy link
Member

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

Copy link
Contributor

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.

Copy link
Member

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

Copy link
Member

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

@onurays onurays linked an issue Oct 25, 2021 that may be closed by this pull request
@onurays onurays requested a review from bmarty October 25, 2021 09:17
Copy link
Contributor

@poljar poljar left a 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.

@bmarty bmarty requested a review from uhoreg October 27, 2021 08:08
@BillCarsonFr BillCarsonFr force-pushed the feature/ons/fallback_keys branch from 4d4eee4 to aacde9d Compare November 15, 2021 12:52
Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@BillCarsonFr BillCarsonFr force-pushed the feature/ons/fallback_keys branch from 3a34d96 to c092656 Compare December 6, 2021 16:18
@BillCarsonFr BillCarsonFr force-pushed the feature/ons/fallback_keys branch from c092656 to 5d35f02 Compare December 7, 2021 18:56
@BillCarsonFr BillCarsonFr requested review from poljar and bmarty December 8, 2021 08:10
Copy link
Contributor

@poljar poljar left a 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()
Copy link
Contributor

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.

Copy link
Member

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()) {
Copy link
Contributor

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.

Copy link
Member

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) {
Copy link
Contributor

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?

Copy link
Member

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

) {
// 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
Copy link
Contributor

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Contributor

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.

Copy link
Contributor

@poljar poljar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Copy link
Member

@bmarty bmarty left a 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)
Copy link
Member

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()
Copy link
Member

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)
Copy link
Member

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()
Copy link
Member

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 :/

@bmarty
Copy link
Member

bmarty commented Dec 13, 2021

Latest remarks will be handled later... or not.

@bmarty bmarty merged commit fa06005 into develop Dec 13, 2021
@bmarty bmarty deleted the feature/ons/fallback_keys branch December 13, 2021 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

MSC2732: Olm fallback keys
4 participants