Skip to content

Commit

Permalink
Merge pull request #260 from matrix-org/delay_otk_generation
Browse files Browse the repository at this point in the history
Delay otk generation
  • Loading branch information
manuroe authored Mar 7, 2017
2 parents b4085ff + 4564620 commit 2445f28
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 295 deletions.
275 changes: 161 additions & 114 deletions MatrixSDK/Crypto/MXCrypto.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@

#ifdef MX_CRYPTO

// Frequency with which to check & upload one-time keys
NSTimeInterval kMXCryptoUploadOneTimeKeysPeriod = 60.0; // one minute

@interface MXCrypto ()
{
// The Matrix session.
Expand All @@ -63,8 +66,11 @@ @interface MXCrypto ()
// @TODO: could be removed
NSDictionary *lastPublishedOneTimeKeys;

// Timer to periodically upload keys
NSTimer *uploadKeysTimer;
// Last time we check available one-time keys on the homeserver
NSDate *lastOneTimeKeyCheck;

// The current one-time key operation, if any
MXHTTPOperation *uploadOneTimeKeysOperation;

// The operation used for crypto starting requests
MXHTTPOperation *startOperation;
Expand Down Expand Up @@ -198,15 +204,15 @@ - (void)start:(void (^)())success
NSMutableDictionary *roomsByUser = [self usersToMakeAnnouncement];

// Start uploading user device keys
startOperation = [self uploadKeys:5 success:^{
startOperation = [self uploadDeviceKeys:^(MXKeysUploadResponse *keysUploadResponse) {

if (!startOperation)
{
return;
}

NSLog(@"[MXCrypto] start ###########################################################");
NSLog(@" uploadKeys done for %@: ", mxSession.myUser.userId);
NSLog(@" uploadDeviceKeys done for %@: ", mxSession.myUser.userId);

NSLog(@" - device id : %@", _store.deviceId);
NSLog(@" - ed25519 : %@", _olmDevice.deviceEd25519Key);
Expand All @@ -216,37 +222,54 @@ - (void)start:(void (^)())success
NSLog(@"Store: %@", _store);
NSLog(@"");

// Once keys are uploaded, make sure we announce ourselves
MXHTTPOperation *operation2 = [self makeAnnoucement:roomsByUser success:^{
// Anounce ourselves if not already done
if (roomsByUser)
{
// But upload our one-time keys before.
// Thus, other devices can download them once they receive our to-device annoucement event
[self maybeUploadOneTimeKeys:^{

// Start periodic timer for uploading keys
uploadKeysTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:10 * 60]
interval:10 * 60 // 10 min
target:self
selector:@selector(uploadKeys)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:uploadKeysTimer forMode:NSDefaultRunLoopMode];
// Once keys are uploaded, announce ourselves
MXHTTPOperation *operation2 = [self makeAnnoucement:roomsByUser success:^{

dispatch_async(dispatch_get_main_queue(), ^{
startOperation = nil;
success();
});
// Make sure we are refreshing devices lists right instead
// of waiting for the next /sync that may occur in 30s.
[_deviceList refreshOutdatedDeviceLists];

} failure:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
startOperation = nil;
success();
});

} failure:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
startOperation = nil;
failure(error);
});
}];

if (operation2)
{
[startOperation mutateTo:operation2];
}
} failure:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
startOperation = nil;
failure(error);
});
}];
}
else
{
// No annoucement require
dispatch_async(dispatch_get_main_queue(), ^{
startOperation = nil;
failure(error);
success();
});
}];

if (operation2)
{
[startOperation mutateTo:operation2];
}

} failure:^(NSError *error) {
NSLog(@"[MXCrypto] start. Error in uploadKeys");
NSLog(@"[MXCrypto] start. Error in uploadDeviceKeys");
dispatch_async(dispatch_get_main_queue(), ^{
startOperation = nil;
failure(error);
Expand All @@ -269,9 +292,9 @@ - (void)close

dispatch_async(_cryptoQueue, ^{

// Stop timer
[uploadKeysTimer invalidate];
uploadKeysTimer = nil;
// Cancel pending one-time keys upload
[uploadOneTimeKeysOperation cancel];
uploadOneTimeKeysOperation = nil;

_olmDevice = nil;
_cryptoQueue = nil;
Expand Down Expand Up @@ -425,6 +448,8 @@ - (void)handleDeviceListsChanged:(NSArray<NSString *>*)userIds oldSyncToken:(NSS

NSLog(@"[MXCrypto] handleDeviceListsChanged: %@", userIds);

BOOL isCatchingUp = mxSession.catchingUp;

dispatch_async(_cryptoQueue, ^{

// Flag users to refresh
Expand Down Expand Up @@ -482,14 +507,16 @@ - (void)handleDeviceListsChanged:(NSArray<NSString *>*)userIds oldSyncToken:(NSS
[_deviceList refreshOutdatedDeviceLists];
}

// @TODO
// we don't start uploading one-time keys until we've caught up with
// We don't start uploading one-time keys until we've caught up with
// to-device messages, to help us avoid throwing away one-time-keys that we
// are about to receive messages for
// (https://github.com/vector-im/riot-web/issues/2782).
// if (!syncData.catchingUp) {
// _maybeUploadOneTimeKeys(this);
// }
// Also, do not upload them if we have not announced our device yet.
// They will be uploaded just before the announcement in [self start].
if (!isCatchingUp && _store.deviceAnnounced)
{
[self maybeUploadOneTimeKeys:nil failure:nil];
}
});

#endif
Expand Down Expand Up @@ -989,86 +1016,6 @@ - (instancetype)initWithMatrixSession:(MXSession*)matrixSession cryptoQueue:(dis
return self;
}

- (MXHTTPOperation *)uploadKeys:(NSUInteger)maxKeys
success:(void (^)())success
failure:(void (^)(NSError *))failure
{
MXHTTPOperation *operation;
operation = [self uploadDeviceKeys:^(MXKeysUploadResponse *keysUploadResponse) {

// We need to keep a pool of one time public keys on the server so that
// other devices can start conversations with us. But we can only store
// a finite number of private keys in the olm Account object.
// To complicate things further then can be a delay between a device
// claiming a public one time key from the server and it sending us a
// message. We need to keep the corresponding private key locally until
// we receive the message.
// But that message might never arrive leaving us stuck with duff
// private keys clogging up our local storage.
// So we need some kind of enginering compromise to balance all of
// these factors.

// We first find how many keys the server has for us.
NSUInteger keyCount = [keysUploadResponse oneTimeKeyCountsForAlgorithm:@"signed_curve25519"];

// We then check how many keys we can store in the Account object.
CGFloat maxOneTimeKeys = _olmDevice.maxNumberOfOneTimeKeys;

// Try to keep at most half that number on the server. This leaves the
// rest of the slots free to hold keys that have been claimed from the
// server but we haven't recevied a message for.
// If we run out of slots when generating new keys then olm will
// discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message.
NSUInteger keyLimit = floor(maxOneTimeKeys / 2);

// We work out how many new keys we need to create to top up the server
// If there are too many keys on the server then we don't need to
// create any more keys.
NSUInteger numberToGenerate = MAX(keyLimit - keyCount, 0);

if (maxKeys)
{
// Creating keys can be an expensive operation so we limit the
// number we generate in one go to avoid blocking the application
// for too long.
numberToGenerate = MIN(numberToGenerate, maxKeys);

// Ask olm to generate new one time keys, then upload them to synapse.
[_olmDevice generateOneTimeKeys:numberToGenerate];
MXHTTPOperation *operation2 = [self uploadOneTimeKeys:success failure:failure];

// Mutate MXHTTPOperation so that the user can cancel this new operation
[operation mutateTo:operation2];
}
else
{
// If we don't need to generate any keys then we are done.
success();
}

if (numberToGenerate <= 0) {
return;
}

} failure:^(NSError *error) {
NSLog(@"[MXCrypto] uploadDeviceKeys fails.");
failure(error);
}];

return operation;
}

- (void)uploadKeys
{
NSLog(@"[MXCrypto] Periodic uploadKeys");

[self uploadKeys:5 success:^{
} failure:^(NSError *error) {
NSLog(@"[MXCrypto] Periodic uploadKeys failed.");
}];
}

- (MXDeviceInfo *)eventSenderDeviceOfEvent:(MXEvent *)event
{
NSString *senderKey = event.senderKey;
Expand Down Expand Up @@ -1776,6 +1723,106 @@ - (MXHTTPOperation *)uploadDeviceKeys:(void (^)(MXKeysUploadResponse *keysUpload
return [_matrixRestClient uploadKeys:myDevice.JSONDictionary oneTimeKeys:nil forDevice:myDevice.deviceId success:success failure:failure];
}

/**
Check if it's time to upload one-time keys, and do so if so.
*/
- (void)maybeUploadOneTimeKeys:(void (^)())success failure:(void (^)(NSError *))failure
{
if (uploadOneTimeKeysOperation)
{
return;
}

NSDate *now = [NSDate date];
if (lastOneTimeKeyCheck && [now timeIntervalSinceDate:lastOneTimeKeyCheck] < kMXCryptoUploadOneTimeKeysPeriod)
{
// We've done a key upload recently.
return;
}

lastOneTimeKeyCheck = now;

NSLog(@"[MXCrypto] maybeUploadOneTimeKeys: Checking available one-time keys on the homeserver");

uploadOneTimeKeysOperation = [_matrixRestClient uploadKeys:myDevice.JSONDictionary oneTimeKeys:nil forDevice:myDevice.deviceId success:^(MXKeysUploadResponse *keysUploadResponse) {

if (!uploadOneTimeKeysOperation)
{
return;
}

// We need to keep a pool of one time public keys on the server so that
// other devices can start conversations with us. But we can only store
// a finite number of private keys in the olm Account object.
// To complicate things further then can be a delay between a device
// claiming a public one time key from the server and it sending us a
// message. We need to keep the corresponding private key locally until
// we receive the message.
// But that message might never arrive leaving us stuck with duff
// private keys clogging up our local storage.
// So we need some kind of enginering compromise to balance all of
// these factors.

// We first find how many keys the server has for us.
NSUInteger keyCount = [keysUploadResponse oneTimeKeyCountsForAlgorithm:@"signed_curve25519"];

NSLog(@"[MXCrypto] maybeUploadOneTimeKeys: one-time keys on the homeserver: %tu", keyCount);

// We then check how many keys we can store in the Account object.
CGFloat maxOneTimeKeys = _olmDevice.maxNumberOfOneTimeKeys;

// Try to keep at most half that number on the server. This leaves the
// rest of the slots free to hold keys that have been claimed from the
// server but we haven't recevied a message for.
// If we run out of slots when generating new keys then olm will
// discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message.
NSUInteger keyLimit = floor(maxOneTimeKeys / 2);

// We work out how many new keys we need to create to top up the server
// If there are too many keys on the server then we don't need to
// create any more keys.
NSUInteger numberToGenerate = MAX(keyLimit - keyCount, 0);
if (numberToGenerate)
{
// Ask olm to generate new one time keys, then upload them to synapse.
[_olmDevice generateOneTimeKeys:numberToGenerate];
MXHTTPOperation *operation2 = [self uploadOneTimeKeys:^(MXKeysUploadResponse *keysUploadResponse) {

if (success)
{
success();
}
else
{
uploadOneTimeKeysOperation = nil;
}

} failure:^(NSError *error) {
NSLog(@"[MXCrypto] maybeUploadOneTimeKeys: Failed to publish one-time keys. Error: %@", error);
uploadOneTimeKeysOperation = nil;

if (failure)
{
failure(error);
}
}];

// Mutate MXHTTPOperation so that the user can cancel this new operation
[uploadOneTimeKeysOperation mutateTo:operation2];
}
else
{
// If we don't need to generate any keys then we are done.
uploadOneTimeKeysOperation = nil;
}

} failure:^(NSError *error) {
NSLog(@"[MXCrypto] maybeUploadOneTimeKeys: Get published one-time keys count failed. Error: %@", error);
uploadOneTimeKeysOperation = nil;
}];
}

/**
Upload my user's one time keys.
*/
Expand Down
14 changes: 0 additions & 14 deletions MatrixSDK/Crypto/MXCrypto_Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,6 @@
*/
@property (nonatomic, readonly) dispatch_queue_t decryptionQueue;

/**
Upload the device keys to the homeserver and ensure that the
homeserver has enough one-time keys.
@param maxKeys The maximum number of keys to generate.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
@return a MXHTTPOperation instance.
*/
- (MXHTTPOperation*)uploadKeys:(NSUInteger)maxKeys
success:(void (^)())success
failure:(void (^)(NSError *))failure;

/**
Get the device which sent an event.
Expand Down
Loading

0 comments on commit 2445f28

Please sign in to comment.