Fix UD auth edge cases.

pull/1/head
Matthew Chen 7 years ago
parent 398e72c69f
commit 4d89670f19

@ -137,20 +137,36 @@ public class ProfileFetcherJob: NSObject {
let unidentifiedAccess: SSKUnidentifiedAccess? = self.getUnidentifiedAccess(forRecipientId: recipientId)
let socketType: OWSWebSocketType = unidentifiedAccess == nil ? .default : .UD
if socketManager.canMakeRequests(of: socketType) {
let request = OWSRequestFactory.getProfileRequest(recipientId: recipientId, unidentifiedAccess: unidentifiedAccess)
let (promise, resolver) = Promise<SignalServiceProfile>.pending()
let socketSuccess = { (responseObject: Any?) -> Void in
do {
let profile = try SignalServiceProfile(recipientId: recipientId, responseObject: responseObject)
resolver.fulfill(profile)
} catch {
resolver.reject(error)
}
}
let request = OWSRequestFactory.getProfileRequest(recipientId: recipientId, unidentifiedAccess: unidentifiedAccess)
self.socketManager.make(request,
webSocketType: socketType,
success: { (responseObject: Any?) -> Void in
do {
let profile = try SignalServiceProfile(recipientId: recipientId, responseObject: responseObject)
resolver.fulfill(profile)
} catch {
resolver.reject(error)
success: socketSuccess,
failure: { (statusCode: NSInteger, _:Data?, error: Error) in
// If UD auth fails, try again with non-UD auth.
if unidentifiedAccess != nil && (statusCode == 401 || statusCode == 403) {
Logger.info("Profile request failing over to non-UD auth.")
let nonUDRequest = OWSRequestFactory.getProfileRequest(recipientId: recipientId, unidentifiedAccess: nil)
self.socketManager.make(nonUDRequest,
webSocketType: .default,
success: socketSuccess,
failure: { (_: NSInteger, _:Data?, error: Error) in
resolver.reject(error)
})
return
}
},
failure: { (_: NSInteger, _:Data?, error: Error) in
resolver.reject(error)
})
return promise

@ -150,7 +150,12 @@ NS_ASSUME_NONNULL_BEGIN
OWSLogDebug(@"removing devices: %@, from registered recipient: %@", devices, latest.recipientId);
[latest removeDevices:devices];
[latest saveWithTransaction_internal:transaction];
if (latest.devices.count > 0) {
[latest saveWithTransaction_internal:transaction];
} else {
[SignalRecipient removeUnregisteredRecipient:self.recipientId transaction:transaction];
}
}
- (NSString *)recipientId

@ -33,10 +33,16 @@ public class OWSMessageSend: NSObject {
// We "fail over" to non-UD sends after auth errors sending via UD.
@objc
public var hasUDAuthFailed = false
public var hasUDAuthFailed = false {
didSet {
if hasUDAuthFailed {
unidentifiedAccess = nil
}
}
}
@objc
public let unidentifiedAccess: SSKUnidentifiedAccess?
public var unidentifiedAccess: SSKUnidentifiedAccess?
@objc
public let localNumber: String

@ -1264,6 +1264,11 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSProdFail([OWSAnalyticsEvents messageSenderErrorNoMissingOrExtraDevices]);
}
if (missingDevices && missingDevices.count > 0) {
OWSLogInfo(@"Adding missing devices: %@", missingDevices);
[recipient addDevicesToRegisteredRecipient:[NSSet setWithArray:missingDevices] transaction:transaction];
}
if (extraDevices && extraDevices.count > 0) {
OWSLogInfo(@"removing extra devices: %@", extraDevices);
for (NSNumber *extraDeviceId in extraDevices) {
@ -1275,12 +1280,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[recipient removeDevicesFromRecipient:[NSSet setWithArray:extraDevices] transaction:transaction];
}
if (missingDevices && missingDevices.count > 0) {
OWSLogInfo(@"Adding missing devices: %@", missingDevices);
[recipient addDevicesToRegisteredRecipient:[NSSet setWithArray:missingDevices]
transaction:transaction];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completionHandler();
});
@ -1369,6 +1368,20 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
for (NSNumber *deviceId in deviceIds) {
@try {
[self ensureRecipientHasSessionForMessageSend:messageSend deviceId:deviceId];
} @catch (NSException *exception) {
if ([exception.name isEqualToString:OWSMessageSenderInvalidDeviceException]) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[recipient removeDevicesFromRecipient:[NSSet setWithObject:deviceId] transaction:transaction];
}];
continue;
} else {
@throw exception;
}
}
@
try {
__block NSDictionary *messageDict;
__block NSException *encryptionException;
[self.dbConnection
@ -1410,84 +1423,133 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// NOTE: This method uses exceptions for control flow.
- (void)ensureRecipientHasSessionForMessageSend:(OWSMessageSend *)messageSend
deviceId:(NSNumber *)deviceId
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(messageSend);
OWSAssertDebug(deviceId);
OWSAssertDebug(transaction);
OWSPrimaryStorage *storage = self.primaryStorage;
SignalRecipient *recipient = messageSend.recipient;
NSString *recipientId = recipient.recipientId;
OWSAssertDebug(recipientId.length > 0);
if (![storage containsSession:recipientId deviceId:[deviceId intValue] protocolContext:transaction]) {
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block PreKeyBundle *_Nullable bundle;
__block NSException *_Nullable exception;
// It's not ideal that we're using a semaphore inside a read/write transaction.
// To avoid deadlock, we need to ensure that our success/failure completions
// are called _off_ the main thread. Otherwise we'll deadlock if the main
// thread is blocked on opening a transaction.
TSRequest *request = [OWSRequestFactory recipientPrekeyRequestWithRecipient:recipientId
deviceId:[deviceId stringValue]
unidentifiedAccess:messageSend.unidentifiedAccess];
__block BOOL hasSession;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
hasSession = [storage containsSession:recipientId deviceId:[deviceId intValue] protocolContext:transaction];
}];
if (hasSession) {
return;
}
[self.networkManager makeRequest:request
completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
success:^(NSURLSessionDataTask *task, id responseObject) {
bundle = [PreKeyBundle preKeyBundleFromDictionary:responseObject forDeviceNumber:deviceId];
dispatch_semaphore_signal(sema);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
if (!IsNSErrorNetworkFailure(error)) {
OWSProdError([OWSAnalyticsEvents messageSenderErrorRecipientPrekeyRequestFailed]);
}
OWSLogError(@"Server replied to PreKeyBundle request with error: %@", error);
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
if (response.statusCode == 404) {
// Can't throw exception from within callback as it's probabably a different thread.
exception = [NSException exceptionWithName:OWSMessageSenderInvalidDeviceException
reason:@"Device not registered"
userInfo:nil];
} else if (response.statusCode == 413) {
// Can't throw exception from within callback as it's probabably a different thread.
exception = [NSException exceptionWithName:OWSMessageSenderRateLimitedException
reason:@"Too many prekey requests"
userInfo:nil];
}
dispatch_semaphore_signal(sema);
}];
// FIXME: Currently this happens within a readwrite transaction - meaning our read-write transaction blocks
// on a network request.
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
if (exception) {
@throw exception;
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block PreKeyBundle *_Nullable bundle;
__block NSException *_Nullable exception;
[self makePrekeyRequestForMessageSend:messageSend
deviceId:deviceId
success:^(PreKeyBundle *_Nullable responseBundle) {
bundle = responseBundle;
dispatch_semaphore_signal(sema);
}
failure:^(NSUInteger statusCode) {
if (statusCode == 404) {
// Can't throw exception from within callback as it's probabably a different thread.
exception = [NSException exceptionWithName:OWSMessageSenderInvalidDeviceException
reason:@"Device not registered"
userInfo:nil];
} else if (statusCode == 413) {
// Can't throw exception from within callback as it's probabably a different thread.
exception = [NSException exceptionWithName:OWSMessageSenderRateLimitedException
reason:@"Too many prekey requests"
userInfo:nil];
}
dispatch_semaphore_signal(sema);
}];
// FIXME: Currently this happens within a readwrite transaction - meaning our read-write transaction blocks
// on a network request.
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
if (exception) {
@throw exception;
}
if (!bundle) {
OWSRaiseException(
InvalidVersionException, @"Can't get a prekey bundle from the server with required information");
} else {
SessionBuilder *builder = [[SessionBuilder alloc] initWithSessionStore:storage
preKeyStore:storage
signedPreKeyStore:storage
identityKeyStore:self.identityManager
recipientId:recipientId
deviceId:[deviceId intValue]];
if (!bundle) {
NSString *missingPrekeyBundleException = @"missingPrekeyBundleException";
OWSRaiseException(
missingPrekeyBundleException, @"Can't get a prekey bundle from the server with required information");
} else {
SessionBuilder *builder = [[SessionBuilder alloc] initWithSessionStore:storage
preKeyStore:storage
signedPreKeyStore:storage
identityKeyStore:self.identityManager
recipientId:recipientId
deviceId:[deviceId intValue]];
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@try {
[builder processPrekeyBundle:bundle protocolContext:transaction];
} @catch (NSException *exception) {
if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
OWSRaiseExceptionWithUserInfo(UntrustedIdentityKeyException,
(@{ TSInvalidPreKeyBundleKey : bundle, TSInvalidRecipientKey : recipientId }),
@"");
}
@throw exception;
} @catch (NSException *caughtException) {
exception = caughtException;
}
}];
if (exception) {
if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
OWSRaiseExceptionWithUserInfo(UntrustedIdentityKeyException,
(@{ TSInvalidPreKeyBundleKey : bundle, TSInvalidRecipientKey : recipientId }),
@"");
}
@throw exception;
}
}
}
// NOTE: This method uses exceptions for control flow.
- (void)makePrekeyRequestForMessageSend:(OWSMessageSend *)messageSend
deviceId:(NSNumber *)deviceId
success:(void (^)(PreKeyBundle *_Nullable))success
failure:(void (^)(NSUInteger))failure {
OWSAssertDebug(messageSend);
OWSAssertDebug(deviceId);
SignalRecipient *recipient = messageSend.recipient;
NSString *recipientId = recipient.recipientId;
OWSAssertDebug(recipientId.length > 0);
const BOOL isUDSend = messageSend.isUDSend;
TSRequest *request = [OWSRequestFactory recipientPrekeyRequestWithRecipient:recipientId
deviceId:[deviceId stringValue]
unidentifiedAccess:messageSend.unidentifiedAccess];
[self.networkManager makeRequest:request
completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
success:^(NSURLSessionDataTask *task, id responseObject) {
PreKeyBundle *_Nullable bundle = [PreKeyBundle preKeyBundleFromDictionary:responseObject
forDeviceNumber:deviceId];
success(bundle);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
if (!IsNSErrorNetworkFailure(error)) {
OWSProdError([OWSAnalyticsEvents messageSenderErrorRecipientPrekeyRequestFailed]);
}
OWSLogError(@"request: %@", request);
OWSLogError(@"Server replied to PreKeyBundle request with error: %@", error);
OWSLogFlush();
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
NSUInteger statusCode = response.statusCode;
if (isUDSend && (statusCode == 401 || statusCode == 403)) {
// If a UD send fails due to service response (as opposed to network
// failure), mark recipient as _not_ in UD mode, then retry.
OWSLogDebug(@"UD prekey request failed; failing over to non-UD prekey request.");
// We need to update the UD access mode for this user async to
// avoid deadlock, since this method is called inside a transaction.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.udManager setUnidentifiedAccessMode:UnidentifiedAccessModeDisabled
recipientId:recipient.uniqueId];
});
messageSend.hasUDAuthFailed = YES;
// Try again without UD auth.
[self makePrekeyRequestForMessageSend:messageSend deviceId:deviceId success:success failure:failure];
return;
}
failure(statusCode);
}];
}
// NOTE: This method uses exceptions for control flow.
- (NSDictionary *)encryptedMessageForMessageSend:(OWSMessageSend *)messageSend
@ -1507,9 +1569,13 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
OWSAssertDebug(recipientId.length > 0);
// This may throw an exception.
[self ensureRecipientHasSessionForMessageSend:messageSend
deviceId:deviceId
transaction:transaction];
if (![storage containsSession:recipientId deviceId:[deviceId intValue] protocolContext:transaction]) {
NSString *missingSessionException = @"missingSessionException";
OWSRaiseException(missingSessionException,
@"Unexpectedly missing session for recipient: %@, device: %@",
recipientId,
deviceId);
}
SessionCipher *cipher = [[SessionCipher alloc] initWithSessionStore:storage
preKeyStore:storage

@ -234,7 +234,7 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
networkError.debugDescription,
request);
error.isRetryable = NO;
[self deregisterAfterAuthErrorIfNecessary:task statusCode:statusCode];
[self deregisterAfterAuthErrorIfNecessary:task request:request statusCode:statusCode];
failureBlock(task, error);
break;
}
@ -242,7 +242,7 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
OWSLogError(
@"The server returned an authentication failure: %@, %@", networkError.debugDescription, request);
error.isRetryable = NO;
[self deregisterAfterAuthErrorIfNecessary:task statusCode:statusCode];
[self deregisterAfterAuthErrorIfNecessary:task request:request statusCode:statusCode];
failureBlock(task, error);
break;
}
@ -300,13 +300,24 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
};
}
+ (void)deregisterAfterAuthErrorIfNecessary:(NSURLSessionDataTask *)task statusCode:(NSInteger)statusCode
{
+ (void)deregisterAfterAuthErrorIfNecessary:(NSURLSessionDataTask *)task
request:(TSRequest *)request
statusCode:(NSInteger)statusCode {
OWSLogVerbose(@"Invalid auth: %@", task.originalRequest.allHTTPHeaderFields);
// Distinguish CDS requests.
// We don't want a bad CDS request to trigger "Signal deauth" logic.
if ([task.originalRequest.URL.absoluteString hasPrefix:textSecureServerURL]) {
// We only want to de-register for:
//
// * Auth errors...
// * ...received from Signal service...
// * ...that used standard authorization.
//
// * We don't want want to deregister for:
//
// * CDS requests.
// * Requests using UD auth.
// * etc.
if ([task.originalRequest.URL.absoluteString hasPrefix:textSecureServerURL]
&& request.shouldHaveAuthorizationHeaders) {
[TSAccountManager.sharedInstance setIsDeregistered:YES];
} else {
OWSLogWarn(@"Ignoring %d for URL: %@", (int)statusCode, task.originalRequest.URL.absoluteString);

@ -90,11 +90,39 @@ public class SignalServiceRestClient: NSObject, SignalServiceClient {
}
public func retrieveProfile(recipientId: RecipientIdentifier, unidentifiedAccess: SSKUnidentifiedAccess?) -> Promise<SignalServiceProfile> {
let (promise, resolver) = Promise<(task: URLSessionDataTask, responseObject: Any?)>.pending()
let request = OWSRequestFactory.getProfileRequest(recipientId: recipientId, unidentifiedAccess: unidentifiedAccess)
return firstly {
networkManager.makePromise(request: request)
}.map { _, responseObject in
try SignalServiceProfile(recipientId: recipientId, responseObject: responseObject)
networkManager.makeRequest(request,
success: { task, responseObject in
resolver.fulfill((task: task, responseObject: responseObject))
},
failure: { task, error in
let statusCode = task.statusCode()
if unidentifiedAccess != nil && (statusCode == 401 || statusCode == 403) {
Logger.verbose("REST profile request failing over to non-UD auth.")
let nonUDRequest = OWSRequestFactory.getProfileRequest(recipientId: recipientId, unidentifiedAccess: nil)
self.networkManager.makeRequest(nonUDRequest,
success: { task, responseObject in
resolver.fulfill((task: task, responseObject: responseObject))
},
failure: { task, error in
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = (error as NSError).isRetryable
resolver.reject(nsError)
})
return
}
Logger.info("REST profile request failed.")
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = (error as NSError).isRetryable
resolver.reject(nsError)
})
return promise.map { _, responseObject in
Logger.info("REST profile request succeeded.")
return try SignalServiceProfile(recipientId: recipientId, responseObject: responseObject)
}
}
}

@ -487,8 +487,9 @@ NSString *const kNSNotification_OWSWebSocketStateDidChange = @"kNSNotification_O
OWSAssertDebug(success);
OWSAssertDebug(failure);
TSSocketMessage *socketMessage =
[[TSSocketMessage alloc] initWithRequestId:[Cryptography randomUInt64] success:success failure:failure];
TSSocketMessage *socketMessage = [[TSSocketMessage alloc] initWithRequestId:[Cryptography randomUInt64]
success:success
failure:failure];
@synchronized(self) {
self.socketMessageMap[@(socketMessage.requestId)] = socketMessage;
@ -633,7 +634,7 @@ NSString *const kNSNotification_OWSWebSocketStateDidChange = @"kNSNotification_O
[socketMessage didSucceedWithResponseObject:responseObject];
} else {
if (responseStatus == 403) {
if (responseStatus == 403 && self.webSocketType == OWSWebSocketTypeDefault) {
// This should be redundant with our check for the socket
// failing due to 403, but let's be thorough.
[self.tsAccountManager setIsDeregistered:YES];
@ -701,7 +702,7 @@ NSString *const kNSNotification_OWSWebSocketStateDidChange = @"kNSNotification_O
if ([error.domain isEqualToString:SRWebSocketErrorDomain] && error.code == 2132) {
NSNumber *_Nullable statusCode = error.userInfo[SRHTTPResponseErrorKey];
if (statusCode.unsignedIntegerValue == 403) {
if (statusCode.unsignedIntegerValue == 403 && self.webSocketType == OWSWebSocketTypeDefault) {
[self.tsAccountManager setIsDeregistered:YES];
}
}

Loading…
Cancel
Save