476 lines
16 KiB
Objective-C
476 lines
16 KiB
Objective-C
//
|
|
// Copyright 2018 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
#import "OWS2FAManager.h"
|
|
#import "AppReadiness.h"
|
|
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
NSString *const NSNotificationName_2FAStateDidChange = @"NSNotificationName_2FAStateDidChange";
|
|
|
|
NSString *const kOWS2FAManager_LastSuccessfulReminderDateKey = @"kOWS2FAManager_LastSuccessfulReminderDateKey";
|
|
NSString *const kOWS2FAManager_PinCode = @"kOWS2FAManager_PinCode";
|
|
NSString *const kOWS2FAManager_RepetitionInterval = @"kOWS2FAManager_RepetitionInterval";
|
|
NSString *const kOWS2FAManager_HasMigratedTruncatedPinKey = @"kOWS2FAManager_HasMigratedTruncatedPinKey";
|
|
NSString *const kOWS2FAManager_AreRemindersEnabled = @"kOWS2FAManager_AreRemindersEnabled";
|
|
|
|
const NSUInteger kHourSecs = 60 * 60;
|
|
const NSUInteger kDaySecs = kHourSecs * 24;
|
|
|
|
const NSUInteger kMin2FAPinLength = 4;
|
|
const NSUInteger kMin2FAv2PinLength = 4;
|
|
const NSUInteger kMax2FAv1PinLength = 20; // v2 doesn't have a max length
|
|
const NSUInteger kLegacyTruncated2FAv1PinLength = 16;
|
|
|
|
@interface OWS2FAManager ()
|
|
|
|
@property (nonatomic) OWS2FAMode mode;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation OWS2FAManager
|
|
|
|
#pragma mark -
|
|
|
|
+ (SDSKeyValueStore *)keyValueStore
|
|
{
|
|
NSString *const kOWS2FAManager_Collection = @"kOWS2FAManager_Collection";
|
|
return [[SDSKeyValueStore alloc] initWithCollection:kOWS2FAManager_Collection];
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
OWSSingletonAssert();
|
|
|
|
AppReadinessRunNowOrWhenMainAppDidBecomeReadyAsync(^{
|
|
if (self.mode == OWS2FAMode_V1) {
|
|
OWSLogInfo(@"Migrating V1 reglock to V2 reglock");
|
|
|
|
[self migrateToRegistrationLockV2]
|
|
.done(^(id value) { OWSLogInfo(@"Successfully migrated to registration lock V2"); })
|
|
.catch(^(NSError *error) {
|
|
OWSFailDebug(@"Failed to migrate V1 reglock to V2 reglock: %@", error.userErrorDescription);
|
|
});
|
|
}
|
|
});
|
|
|
|
return self;
|
|
}
|
|
|
|
- (nullable NSString *)pinCode
|
|
{
|
|
__block NSString *_Nullable value;
|
|
[self.databaseStorage
|
|
readWithBlock:^(SDSAnyReadTransaction *transaction) { value = [self pinCodeWithTransaction:transaction]; }];
|
|
return value;
|
|
}
|
|
|
|
- (nullable NSString *)pinCodeWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
return [OWS2FAManager.keyValueStore getString:kOWS2FAManager_PinCode transaction:transaction];
|
|
}
|
|
|
|
- (void)setPinCode:(NSString *)pin transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
if (pin.length == 0) {
|
|
[self clearLocalPinCodeWithTransaction:transaction];
|
|
return;
|
|
}
|
|
|
|
if ([self hasBackedUpMasterKeyWithTransaction:transaction]) {
|
|
pin = [SVRUtil normalizePin:pin];
|
|
} else {
|
|
// Convert the pin to arabic numerals, we never want to
|
|
// operate with pins in other numbering systems.
|
|
pin = pin.ensureArabicNumerals;
|
|
}
|
|
|
|
[OWS2FAManager.keyValueStore setString:pin key:kOWS2FAManager_PinCode transaction:transaction];
|
|
}
|
|
|
|
- (OWS2FAMode)mode
|
|
{
|
|
__block bool hasBackedUpMasterKey;
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
hasBackedUpMasterKey = [self hasBackedUpMasterKeyWithTransaction:transaction];
|
|
}];
|
|
// Identify what version of 2FA we're using
|
|
if (hasBackedUpMasterKey) {
|
|
return OWS2FAMode_V2;
|
|
} else if (self.pinCode != nil) {
|
|
return OWS2FAMode_V1;
|
|
} else {
|
|
return OWS2FAMode_Disabled;
|
|
}
|
|
}
|
|
|
|
- (BOOL)is2FAEnabled
|
|
{
|
|
return self.mode != OWS2FAMode_Disabled;
|
|
}
|
|
|
|
- (void)markDisabledWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[OWS2FAManager.keyValueStore removeValueForKey:kOWS2FAManager_PinCode transaction:transaction];
|
|
[OWS2FAManager.keyValueStore removeValueForKey:OWS2FAManager.isRegistrationLockV2EnabledKey
|
|
transaction:transaction];
|
|
|
|
[transaction addSyncCompletion:^{
|
|
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationName_2FAStateDidChange
|
|
object:nil
|
|
userInfo:nil];
|
|
|
|
[AccountAttributesUpdaterObjcBridge updateAccountAttributes].catch(
|
|
^(NSError *error) { OWSLogError(@"Error: %@", error); });
|
|
}];
|
|
}
|
|
|
|
- (void)markEnabledWithPin:(NSString *)pin transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self setPinCode:pin transaction:transaction];
|
|
|
|
// Since we just created this pin, we know it doesn't need migration. Mark it as such.
|
|
[self markLegacyPinAsMigratedWithTransaction:transaction];
|
|
|
|
// Reset the reminder repetition interval for the new pin.
|
|
[self setDefaultRepetitionIntervalWithTransaction:transaction];
|
|
|
|
// Schedule next reminder relative to now
|
|
[self setLastCompletedReminderDate:[NSDate new] transaction:transaction];
|
|
|
|
[transaction addSyncCompletion:^{
|
|
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationName_2FAStateDidChange
|
|
object:nil
|
|
userInfo:nil];
|
|
|
|
[AccountAttributesUpdaterObjcBridge updateAccountAttributes].catch(
|
|
^(NSError *error) { OWSLogError(@"Error: %@", error); });
|
|
}];
|
|
}
|
|
|
|
- (void)requestEnable2FAWithPin:(NSString *)pin
|
|
mode:(OWS2FAMode)mode
|
|
success:(nullable OWS2FASuccess)success
|
|
failure:(nullable OWS2FAFailure)failure
|
|
{
|
|
OWSAssertDebug(pin.length > 0);
|
|
OWSAssertDebug(success);
|
|
OWSAssertDebug(failure);
|
|
|
|
switch (mode) {
|
|
case OWS2FAMode_V2: {
|
|
// Enabling V2 2FA doesn't inherently enable registration lock,
|
|
// it's managed by a separate setting.
|
|
[self generateAndBackupKeysWithPin:pin]
|
|
.done(^(id value) {
|
|
OWSAssertIsOnMainThread();
|
|
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[self markEnabledWithPin:pin transaction:transaction];
|
|
});
|
|
|
|
if (success) {
|
|
success();
|
|
}
|
|
})
|
|
.catch(^(NSError *error) {
|
|
OWSAssertIsOnMainThread();
|
|
|
|
if (failure) {
|
|
failure(error);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case OWS2FAMode_V1:
|
|
[self enable2FAV1WithPin:pin success:success failure:failure];
|
|
break;
|
|
case OWS2FAMode_Disabled:
|
|
OWSFailDebug(@"Unexpectedly attempting to enable 2fa for disabled mode");
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void)disable2FAWithSuccess:(nullable OWS2FASuccess)success failure:(nullable OWS2FAFailure)failure
|
|
{
|
|
switch (self.mode) {
|
|
case OWS2FAMode_V2: {
|
|
[self deleteKeys]
|
|
.then(^(id value) { return [self disableRegistrationLockV2]; })
|
|
.ensure(^{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
DatabaseStorageWrite(self.databaseStorage,
|
|
^(SDSAnyWriteTransaction *transaction) { [self markDisabledWithTransaction:transaction]; });
|
|
})
|
|
.done(^(id value) {
|
|
OWSAssertIsOnMainThread();
|
|
|
|
if (success) {
|
|
success();
|
|
}
|
|
})
|
|
.catch(^(NSError *error) {
|
|
OWSAssertIsOnMainThread();
|
|
|
|
if (failure) {
|
|
failure(error);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
case OWS2FAMode_V1:
|
|
[self disable2FAV1WithSuccess:success failure:failure];
|
|
break;
|
|
case OWS2FAMode_Disabled:
|
|
OWSFailDebug(@"Unexpectedly attempting to disable 2fa for disabled mode");
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Reminders
|
|
|
|
- (nullable NSDate *)lastCompletedReminderDateWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
return [OWS2FAManager.keyValueStore getDate:kOWS2FAManager_LastSuccessfulReminderDateKey transaction:transaction];
|
|
}
|
|
|
|
- (void)setLastCompletedReminderDate:(nullable NSDate *)date transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[OWS2FAManager.keyValueStore setDate:date key:kOWS2FAManager_LastSuccessfulReminderDateKey transaction:transaction];
|
|
}
|
|
|
|
- (BOOL)isDueForV2ReminderWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
if (![TSAccountManagerObjcBridge isRegisteredPrimaryDeviceWithMaybeTransaction]) {
|
|
return NO;
|
|
}
|
|
|
|
if (![self hasBackedUpMasterKeyWithTransaction:transaction]) {
|
|
return NO;
|
|
}
|
|
|
|
if ([self pinCodeWithTransaction:transaction].length == 0) {
|
|
OWSLogInfo(@"Missing 2FA pin, prompting for reminder so we can backfill it.");
|
|
return YES;
|
|
}
|
|
|
|
if (![self areRemindersEnabledTransaction:transaction]) {
|
|
return NO;
|
|
}
|
|
|
|
NSDate *nextReminderDate = [self nextReminderDateWithTransaction:transaction];
|
|
|
|
return nextReminderDate.timeIntervalSinceNow < 0;
|
|
}
|
|
|
|
- (BOOL)needsLegacyPinMigration
|
|
{
|
|
__block BOOL hasMigratedTruncatedPin = NO;
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
hasMigratedTruncatedPin = [OWS2FAManager.keyValueStore getBool:kOWS2FAManager_HasMigratedTruncatedPinKey
|
|
defaultValue:NO
|
|
transaction:transaction];
|
|
}];
|
|
|
|
if (hasMigratedTruncatedPin) {
|
|
return NO;
|
|
}
|
|
|
|
// Older versions of the app truncated newly created pins to 16 characters. We no longer do that.
|
|
// If we detect that the user's pin is the truncated length and it was created before we stopped
|
|
// truncating pins, we'll need to ensure we migrate to the user's entire pin next time we prompt
|
|
// them for it.
|
|
if (self.mode == OWS2FAMode_V1 && self.pinCode.length >= kLegacyTruncated2FAv1PinLength) {
|
|
return YES;
|
|
}
|
|
|
|
// We don't need to migrate this pin, either because it's v2 or short enough that
|
|
// we never truncated it. Mark it as complete so we don't need to check again.
|
|
|
|
DatabaseStorageWrite(self.databaseStorage,
|
|
^(SDSAnyWriteTransaction *transaction) { [self markLegacyPinAsMigratedWithTransaction:transaction]; });
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)markLegacyPinAsMigratedWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[OWS2FAManager.keyValueStore setBool:YES key:kOWS2FAManager_HasMigratedTruncatedPinKey transaction:transaction];
|
|
}
|
|
|
|
- (void)verifyPin:(NSString *)pin result:(void (^_Nonnull)(BOOL))result
|
|
{
|
|
NSString *pinToMatch = self.pinCode;
|
|
|
|
switch (self.mode) {
|
|
case OWS2FAMode_V2:
|
|
if (pinToMatch.length > 0) {
|
|
result([pinToMatch isEqualToString:[SVRUtil normalizePin:pin]]);
|
|
} else {
|
|
[self verifyKBSPin:pin
|
|
resultHandler:^(BOOL isValid) {
|
|
result(isValid);
|
|
|
|
if (isValid) {
|
|
OWSLogInfo(@"Verified PIN code");
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[self setPinCode:pin transaction:transaction];
|
|
});
|
|
}
|
|
}];
|
|
}
|
|
break;
|
|
case OWS2FAMode_V1:
|
|
// Convert the pin to arabic numerals, we never want to
|
|
// operate with pins in other numbering systems.
|
|
result([pinToMatch.ensureArabicNumerals isEqualToString:pin.ensureArabicNumerals]);
|
|
break;
|
|
case OWS2FAMode_Disabled:
|
|
OWSFailDebug(@"unexpectedly attempting to verify pin when 2fa is disabled");
|
|
result(NO);
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (NSDate *)nextReminderDate
|
|
{
|
|
__block NSDate *_Nullable value;
|
|
[self.databaseStorage readWithBlock:^(
|
|
SDSAnyReadTransaction *transaction) { value = [self nextReminderDateWithTransaction:transaction]; }];
|
|
return value;
|
|
}
|
|
|
|
- (NSDate *)nextReminderDateWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
NSDate *lastCompletedReminderDate =
|
|
[self lastCompletedReminderDateWithTransaction:transaction] ?: [NSDate distantPast];
|
|
NSTimeInterval repetitionInterval = [self repetitionIntervalWithTransaction:transaction];
|
|
|
|
return [lastCompletedReminderDate dateByAddingTimeInterval:repetitionInterval];
|
|
}
|
|
|
|
- (NSArray<NSNumber *> *)allRepetitionIntervals
|
|
{
|
|
// Keep sorted monotonically increasing.
|
|
return @[
|
|
@(12 * kHourSecs),
|
|
@(1 * kDaySecs),
|
|
@(3 * kDaySecs),
|
|
@(7 * kDaySecs),
|
|
@(14 * kDaySecs),
|
|
];
|
|
}
|
|
|
|
- (double)defaultRepetitionInterval
|
|
{
|
|
return self.allRepetitionIntervals.firstObject.doubleValue;
|
|
}
|
|
|
|
- (NSTimeInterval)repetitionInterval
|
|
{
|
|
__block NSTimeInterval value;
|
|
[self.databaseStorage readWithBlock:^(
|
|
SDSAnyReadTransaction *transaction) { value = [self repetitionIntervalWithTransaction:transaction]; }];
|
|
return value;
|
|
}
|
|
|
|
- (NSTimeInterval)repetitionIntervalWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
return [OWS2FAManager.keyValueStore getDouble:kOWS2FAManager_RepetitionInterval
|
|
defaultValue:self.defaultRepetitionInterval
|
|
transaction:transaction];
|
|
}
|
|
|
|
- (void)reminderCompletedWithIncorrectAttempts:(BOOL)incorrectAttempts
|
|
{
|
|
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[self setLastCompletedReminderDate:[NSDate new] transaction:transaction];
|
|
|
|
NSTimeInterval oldInterval = [self repetitionIntervalWithTransaction:transaction];
|
|
NSTimeInterval newInterval = [self adjustRepetitionInterval:oldInterval
|
|
withIncorrectAttempts:incorrectAttempts];
|
|
|
|
OWSLogInfo(@"Updating repetition interval: %f -> %f. Had incorrect attempts: %d",
|
|
oldInterval,
|
|
newInterval,
|
|
incorrectAttempts);
|
|
|
|
[OWS2FAManager.keyValueStore setDouble:newInterval
|
|
key:kOWS2FAManager_RepetitionInterval
|
|
transaction:transaction];
|
|
});
|
|
}
|
|
|
|
- (NSTimeInterval)adjustRepetitionInterval:(NSTimeInterval)oldInterval withIncorrectAttempts:(BOOL)incorrectAttempts
|
|
{
|
|
NSArray<NSNumber *> *allIntervals = self.allRepetitionIntervals;
|
|
|
|
NSUInteger oldIndex =
|
|
[allIntervals indexOfObjectPassingTest:^BOOL(NSNumber *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
|
|
return oldInterval <= (NSTimeInterval)obj.doubleValue;
|
|
}];
|
|
|
|
NSUInteger newIndex;
|
|
if (!incorrectAttempts) {
|
|
newIndex = oldIndex + 1;
|
|
} else {
|
|
// prevent overflow
|
|
newIndex = oldIndex <= 0 ? 0 : oldIndex - 1;
|
|
}
|
|
|
|
// clamp to be valid
|
|
newIndex = MAX(0, MIN(allIntervals.count - 1, newIndex));
|
|
|
|
NSTimeInterval newInterval = allIntervals[newIndex].doubleValue;
|
|
return newInterval;
|
|
}
|
|
|
|
- (void)setDefaultRepetitionIntervalWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[OWS2FAManager.keyValueStore setDouble:self.defaultRepetitionInterval
|
|
key:kOWS2FAManager_RepetitionInterval
|
|
transaction:transaction];
|
|
}
|
|
|
|
- (BOOL)areRemindersEnabled
|
|
{
|
|
__block BOOL areRemindersEnabled;
|
|
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
|
areRemindersEnabled = [self areRemindersEnabledTransaction:transaction];
|
|
}];
|
|
return areRemindersEnabled;
|
|
}
|
|
|
|
- (BOOL)areRemindersEnabledTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
return [OWS2FAManager.keyValueStore getBool:kOWS2FAManager_AreRemindersEnabled
|
|
defaultValue:YES
|
|
transaction:transaction];
|
|
}
|
|
|
|
- (void)setAreRemindersEnabled:(BOOL)areRemindersEnabled transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
return [OWS2FAManager.keyValueStore setBool:areRemindersEnabled
|
|
key:kOWS2FAManager_AreRemindersEnabled
|
|
transaction:transaction];
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|