diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 9d8a52cfc..6e8d225f5 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -204,6 +204,7 @@ 34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */; }; 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; }; 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */; }; + 34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */; }; 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; }; 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0241ED3673300188D7C /* DebugUIMessages.m */; }; 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; }; @@ -809,6 +810,7 @@ 34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentUploadView.h; sourceTree = ""; }; 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = ""; }; 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientStatusUtils.swift; sourceTree = ""; }; + 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSScreenLock.swift; sourceTree = ""; }; 34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = ""; }; 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvatarViewHelper.m; sourceTree = ""; }; 34D8C0231ED3673300188D7C /* DebugUIMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessages.h; sourceTree = ""; }; @@ -1961,14 +1963,15 @@ 34A9105E1FFEB113000C4745 /* OWSBackup.h */, 34A9105F1FFEB114000C4745 /* OWSBackup.m */, 340FC8C6204DE64D007AEB0F /* OWSBackupAPI.swift */, - 340FC8CF205BF2FA007AEB0F /* OWSBackupIO.h */, - 340FC8CE205BF2FA007AEB0F /* OWSBackupIO.m */, 340FC8BE204DB7D1007AEB0F /* OWSBackupExportJob.h */, 340FC8BF204DB7D2007AEB0F /* OWSBackupExportJob.m */, 340FC8C920517B84007AEB0F /* OWSBackupImportJob.h */, 340FC8C820517B84007AEB0F /* OWSBackupImportJob.m */, + 340FC8CF205BF2FA007AEB0F /* OWSBackupIO.h */, + 340FC8CE205BF2FA007AEB0F /* OWSBackupIO.m */, 340FC8CB20518C76007AEB0F /* OWSBackupJob.h */, 340FC8CC20518C76007AEB0F /* OWSBackupJob.m */, + 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */, 4579431C1E7C8CE9008ED0C0 /* Pastelog.h */, 4579431D1E7C8CE9008ED0C0 /* Pastelog.m */, 450DF2041E0D74AC003D14BE /* Platform.swift */, @@ -3236,6 +3239,7 @@ 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, + 34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */, 340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */, 340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */, 4579431E1E7C8CE9008ED0C0 /* Pastelog.m in Sources */, diff --git a/Signal/src/Jobs/SyncPushTokensJob.swift b/Signal/src/Jobs/SyncPushTokensJob.swift index 043f57004..1817f09ed 100644 --- a/Signal/src/Jobs/SyncPushTokensJob.swift +++ b/Signal/src/Jobs/SyncPushTokensJob.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import PromiseKit @@ -63,9 +63,9 @@ class SyncPushTokensJob: NSObject { } Logger.warn("\(self.TAG) uploading tokens to account servers. pushToken: \(pushToken), voipToken: \(voipToken)") - return self.accountManager.updatePushTokens(pushToken:pushToken, voipToken:voipToken).then { + return self.accountManager.updatePushTokens(pushToken: pushToken, voipToken: voipToken).then { Logger.info("\(self.TAG) successfully updated push tokens on server") - return self.recordPushTokensLocally(pushToken:pushToken, voipToken:voipToken) + return self.recordPushTokensLocally(pushToken: pushToken, voipToken: voipToken) } }.then { Logger.info("\(self.TAG) completed successfully.") @@ -108,9 +108,7 @@ class SyncPushTokensJob: NSObject { } if (didTokensChange) { - DispatchQueue.main.async { - NotificationCenter.default.post(name: SyncPushTokensJob.PushTokensDidChange, object: nil) - } + NotificationCenter.default.postNotificationNameAsync(SyncPushTokensJob.PushTokensDidChange, object: nil) } return Promise(value: ()) diff --git a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m index 97b37ab0f..e5c0a82d0 100644 --- a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m @@ -91,6 +91,10 @@ self.title = NSLocalizedString(@"SETTINGS_NAV_BAR_TITLE", @"Title for settings activity"); [self updateTableContents]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self showPrivacy]; + }); } - (void)viewWillAppear:(BOOL)animated diff --git a/Signal/src/ViewControllers/AppSettings/PrivacySettingsTableViewController.m b/Signal/src/ViewControllers/AppSettings/PrivacySettingsTableViewController.m index 0de1d77e4..0024a4db1 100644 --- a/Signal/src/ViewControllers/AppSettings/PrivacySettingsTableViewController.m +++ b/Signal/src/ViewControllers/AppSettings/PrivacySettingsTableViewController.m @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN self.title = NSLocalizedString(@"SETTINGS_PRIVACY_TITLE", @""); + [self observeNotifications]; + [self updateTableContents]; } @@ -30,6 +32,19 @@ NS_ASSUME_NONNULL_BEGIN [self updateTableContents]; } +- (void)observeNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(screenLockDidChange:) + name:OWSScreenLock.ScreenLockDidChange + object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + #pragma mark - Table Contents - (void)updateTableContents @@ -152,6 +167,27 @@ NS_ASSUME_NONNULL_BEGIN }]]; [contents addSection:twoFactorAuthSection]; + // BOOL showScreenLockUI = OWSScreenLock.sharedManager.isScreenLockSupported; + //#ifdef DEBUG + // showScreenLockUI = YES; + //#endif + BOOL showScreenLockUI = YES; + if (showScreenLockUI) { + OWSTableSection *screenLockSection = [OWSTableSection new]; + screenLockSection.headerTitle = NSLocalizedString( + @"SETTINGS_SCREEN_LOCK_SECTION_TITLE", @"Title for the 'screen lock' section of the privacy settings."); + screenLockSection.footerTitle = NSLocalizedString( + @"SETTINGS_SCREEN_LOCK_SECTION_FOOTER", @"Footer for the 'screen lock' section of the privacy settings."); + [screenLockSection + addItem:[OWSTableItem + switchItemWithText:NSLocalizedString(@"SETTINGS_SCREEN_LOCK_SWITCH_LABEL", + @"Label for the 'enable screen lock' switch of the privacy settings.") + isOn:OWSScreenLock.sharedManager.isScreenLockEnabled + target:self + selector:@selector(isScreenLockEnabledDidChange:)]]; + [contents addSection:screenLockSection]; + } + self.contents = contents; } @@ -257,6 +293,48 @@ NS_ASSUME_NONNULL_BEGIN [self.navigationController pushViewController:vc animated:YES]; } +- (void)isScreenLockEnabledDidChange:(UISwitch *)sender +{ + BOOL shouldBeEnabled = sender.isOn; + + if (shouldBeEnabled == OWSScreenLock.sharedManager.isScreenLockEnabled) { + DDLogError(@"%@ ignoring redundant screen lock.", self.logTag); + return; + } + + DDLogInfo(@"%@ trying to set is screen lock enabled: %@", self.logTag, @(shouldBeEnabled)); + + __weak typeof(self) weakSelf = self; + if (shouldBeEnabled) { + [OWSScreenLock.sharedManager tryToEnableScreenLockWithCompletion:^(NSError *_Nullable error) { + [weakSelf updateTableContents]; + + if (error) { + [OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_ENABLE_FAILED", + @"Title for alert indicating that screen lock could not be enabled.") + message:error.localizedDescription]; + } + }]; + } else { + [OWSScreenLock.sharedManager tryToDisableScreenLockWithCompletion:^(NSError *_Nullable error) { + [weakSelf updateTableContents]; + + if (error) { + [OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_DISABLE_FAILED", + @"Title for alert indicating that screen lock could not be disabled.") + message:error.localizedDescription]; + } + }]; + } +} + +- (void)screenLockDidChange:(NSNotification *)notification +{ + DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); + + [self updateTableContents]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index dccd63e15..497a898c1 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -284,6 +284,10 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } [self updateBarButtonItems]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self settingsButtonPressed:nil]; + }); } - (void)viewDidAppear:(BOOL)animated diff --git a/Signal/src/util/OWSScreenLock.swift b/Signal/src/util/OWSScreenLock.swift new file mode 100644 index 000000000..5908936e9 --- /dev/null +++ b/Signal/src/util/OWSScreenLock.swift @@ -0,0 +1,377 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import LocalAuthentication + +@objc public class OWSScreenLock: NSObject { + + let TAG = "[OWSScreenLock]" + + public enum OWSScreenLockOutcome { + case success + case cancel + case failure(error:String) + } + + @objc public static let ScreenLockDidChange = Notification.Name("ScreenLockDidChange") + + let primaryStorage: OWSPrimaryStorage + let dbConnection: YapDatabaseConnection + + private let OWSScreenLock_Collection = "OWSScreenLock_Collection" + private let OWSScreenLock_Key_IsScreenLockEnabled = "OWSScreenLock_Key_IsScreenLockEnabled" + + // MARK - Singleton class + + @objc(sharedManager) + public static let shared = OWSScreenLock() + + private override init() { + self.primaryStorage = OWSPrimaryStorage.shared() + self.dbConnection = self.primaryStorage.newDatabaseConnection() + + super.init() + + SwiftSingletons.register(self) + } + + @objc public func isScreenLockEnabled() -> Bool { + AssertIsOnMainThread() + + return self.dbConnection.bool(forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection, defaultValue: false) + } + + private func setIsScreenLockEnabled(value: Bool) { + AssertIsOnMainThread() + + self.dbConnection.setBool(value, forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection) + + NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil) + } + +// @objc public func isScreenLockSupported() -> Bool { +// AssertIsOnMainThread() +// +// let context = screenLockContext() +// +// var authError: NSError? +// let result = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) +// guard authError == nil else { +// Logger.error("\(TAG) could not determine if screen lock is supported: \(String(describing: authError))") +// return false +// } +// return result +// } + +// @objc public func supportedBiometryType() -> LABiometryType { +// AssertIsOnMainThread() +// +// let context = screenLockContext() +// +// var authError: NSError? +// let result = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) +// guard authError == nil else { +// Logger.error("\(TAG) could not determine if screen lock is supported: \(String(describing: authError))") +// return .none +// } +// guard let biometryType = context.biometryType else { +// return .none +// } +// return biometryType +// } + + // On failure, completion is called with an error argument. + // On success or cancel, completion is called with nil argument. + // Success and cancel can be differentiated by consulting + // isScreenLockEnabled. + @objc public func tryToEnableScreenLock(completion: @escaping ((Error?) -> Void)) { + tryToVerifyLocalAuthentication(defaultReason: NSLocalizedString("SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK", + comment: "Description of how and why Signal iOS uses Touch ID/Face ID to enable 'screen lock'."), + touchIdReason: NSLocalizedString("SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK_TOUCH_ID", + comment: "Description of how and why Signal iOS uses Touch ID to enable 'screen lock'."), + faceIdReason: NSLocalizedString("SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK_FACE_ID", + comment: "Description of how and why Signal iOS uses Face ID to enable 'screen lock'."), + + completion: { (outcome: OWSScreenLockOutcome) in + AssertIsOnMainThread() + + switch outcome { + case .failure(let error): + completion(self.authenticationError(errorDescription: error)) + case .success: + self.setIsScreenLockEnabled(value: true) + completion(nil) + case .cancel: + completion(nil) + } + }) + } + + // On failure, completion is called with an error argument. + // On success or cancel, completion is called with nil argument. + // Success and cancel can be differentiated by consulting + // isScreenLockEnabled. + @objc public func tryToDisableScreenLock(completion: @escaping ((Error?) -> Void)) { + tryToVerifyLocalAuthentication(defaultReason: NSLocalizedString("SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK", + comment: "Description of how and why Signal iOS uses Touch ID/Face ID to disable 'screen lock'."), + touchIdReason: NSLocalizedString("SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK_TOUCH_ID", + comment: "Description of how and why Signal iOS uses Touch ID to disable 'screen lock'."), + faceIdReason: NSLocalizedString("SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK_FACE_ID", + comment: "Description of how and why Signal iOS uses Face ID to disable 'screen lock'."), + + completion: { (outcome: OWSScreenLockOutcome) in + AssertIsOnMainThread() + + switch outcome { + case .failure(let error): + completion(self.authenticationError(errorDescription: error)) + case .success: + self.setIsScreenLockEnabled(value: false) + completion(nil) + case .cancel: + completion(nil) + } + }) + } + + // On failure, completion is called with an error argument. + // On success or cancel, completion is called with nil argument. + // Success and cancel can be differentiated by consulting + // isScreenLockEnabled. + private func tryToVerifyLocalAuthentication(defaultReason: String, + touchIdReason: String, + faceIdReason: String, + completion completionParam: @escaping ((OWSScreenLockOutcome) -> Void)) { + + // Ensure completion is always called on the main thread. + let completion = { (outcome: OWSScreenLockOutcome) in + switch outcome { + case .failure(let error): + Logger.error("\(self.TAG) enable screen lock failed with error: \(error)") + default: + break + } + DispatchQueue.main.async { + completionParam(outcome) + } + } + + let context = screenLockContext() + let defaultErrorDescription = NSLocalizedString("SCREEN_LOCK_ENABLE_UNKNOWN_ERROR", + comment: "Indicates that an unknown error occurred while using Touch ID or Face ID.") + + var authError: NSError? + let canEvaluatePolicy = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) + if !canEvaluatePolicy || authError != nil { + Logger.error("\(TAG) could not determine if screen lock is supported: \(String(describing: authError))") + + let outcome = self.outcomeForLAError(errorParam: authError, + defaultErrorDescription: defaultErrorDescription) + switch outcome { + case .success: + owsFail("\(self.TAG) unexpected success") + completion(.failure(error:defaultErrorDescription)) + case .cancel, .failure: + completion(outcome) + } + return + } + + var localizedReason = defaultReason + if #available(iOS 11.0, *) { + if context.biometryType == .touchID { + localizedReason = touchIdReason + } else if context.biometryType == .faceID { + localizedReason = faceIdReason + } + } + + context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: localizedReason) { success, evaluateError in + if success { + Logger.info("\(self.TAG) enable screen lock succeeded.") + completion(.success) + } else { + let outcome = self.outcomeForLAError(errorParam: evaluateError, + defaultErrorDescription: defaultErrorDescription) + switch outcome { + case .success: + owsFail("\(self.TAG) unexpected success") + completion(.failure(error:defaultErrorDescription)) + case .cancel, .failure: + completion(outcome) + } + } + } + } + + // MARK: - Outcome + + private func outcomeForLAError(errorParam: Error?, defaultErrorDescription: String) -> OWSScreenLockOutcome { + if let error = errorParam { + guard let laError = error as? LAError else { + return .failure(error:defaultErrorDescription) + } + + // TODO: I'm not sure this is necessary. + if #available(iOS 11.0, *) { + switch laError.code { + case .biometryNotAvailable: + Logger.error("\(self.TAG) local authentication error: biometryNotAvailable.") + return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE", + comment: "Indicates that Touch ID/Face ID are not available on this device.")) + case .biometryNotEnrolled: + Logger.error("\(self.TAG) local authentication error: biometryNotEnrolled.") + return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED", + comment: "Indicates that Touch ID/Face ID is not configured on this device.")) + case .biometryLockout: + Logger.error("\(self.TAG) local authentication error: biometryLockout.") + return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT", + comment: "Indicates that Touch ID/Face ID is 'locked out' on this device due to authentication failures.")) + default: + // Fall through to second switch + break + } + } + + switch laError.code { + case .authenticationFailed: + Logger.error("\(self.TAG) local authentication error: authenticationFailed.") + return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED", + comment: "Indicates that Touch ID/Face ID authentication failed.")) + case .userCancel, .userFallback, .systemCancel, .appCancel: + Logger.info("\(self.TAG) local authentication cancelled.") + return .cancel + case .passcodeNotSet: + Logger.error("\(self.TAG) local authentication error: passcodeNotSet.") + return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET", + comment: "Indicates that Touch ID/Face ID passcode is not set.")) + case .touchIDNotAvailable: + Logger.error("\(self.TAG) local authentication error: touchIDNotAvailable.") + return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE", + comment: "Indicates that Touch ID/Face ID are not available on this device.")) + case .touchIDNotEnrolled: + Logger.error("\(self.TAG) local authentication error: touchIDNotEnrolled.") + return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED", + comment: "Indicates that Touch ID/Face ID is not configured on this device.")) + case .touchIDLockout: + Logger.error("\(self.TAG) local authentication error: touchIDLockout.") + return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT", + comment: "Indicates that Touch ID/Face ID is 'locked out' on this device due to authentication failures.")) + case .invalidContext: + owsFail("\(self.TAG) context not valid.") + break + case .notInteractive: + owsFail("\(self.TAG) context not interactive.") + break + } + // This switch may fall through. + } + return .failure(error:defaultErrorDescription) + } + + private func authenticationError(errorDescription: String) -> Error { + return OWSErrorWithCodeDescription(.localAuthenticationError, + errorDescription) + } + + // MARK: - Context + + private func screenLockContext() -> LAContext { + let context = LAContext() + + // if #available(iOS 11, *) { + // context.biometryType = [.touchId,.faceId] + // } + + // Time interval for accepting a successful Touch ID or Face ID device unlock (on the lock screen) from the past. + // + // TODO: Review. + context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(5.0) + + // Don't set context.maxBiometryFailures. + // + // TODO: Review. + + return context + } + // /// Fallback button title. + // /// @discussion Allows fallback button title customization. If set to empty string, the button will be hidden. + // /// A default title "Enter Password" is used when this property is left nil. + // @property (nonatomic, nullable, copy) NSString *localizedFallbackTitle; + // + // /// Cancel button title. + // /// @discussion Allows cancel button title customization. A default title "Cancel" is used when + // /// this property is left nil or is set to empty string. + // @property (nonatomic, nullable, copy) NSString *localizedCancelTitle NS_AVAILABLE(10_12, 10_0); + // + // + // typedef NS_ENUM(NSInteger, LAAccessControlOperation) + // { + // /// Access control will be used for item creation. + // LAAccessControlOperationCreateItem, + // + // /// Access control will be used for accessing existing item. + // LAAccessControlOperationUseItem, + // + // /// Access control will be used for key creation. + // LAAccessControlOperationCreateKey, + // + // /// Access control will be used for sign operation with existing key. + // LAAccessControlOperationUseKeySign, + // + // /// Access control will be used for data decryption using existing key. + // LAAccessControlOperationUseKeyDecrypt NS_ENUM_AVAILABLE(10_12, 10_0), + // + // /// Access control will be used for key exchange. + // LAAccessControlOperationUseKeyKeyExchange NS_ENUM_AVAILABLE(10_12, 10_0), + // } NS_ENUM_AVAILABLE(10_11, 9_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0); + // + // + // + // typedef NS_ENUM(NSInteger, LAPolicy) + // { + // /// Device owner is going to be authenticated using a biometric method (Touch ID or Face ID). + // /// + // /// @discussion Biometric authentication is required. If the biometry is not available, not enrolled, + // /// or locked out, then the evaluation of this policy will fail with LAErrorBiometryNotAvailable, + // /// LAErrorBiometryNotEnrolled or LAErrorBiometryLockout. Biometry can't be used without a passcode, + // /// i.e. when biometry is available, enrolled and not locked out but passcode is not set, then + // /// the evaluation will fail with LAErrorPasscodeNotSet. + // /// + // /// Touch ID authentication dialog contains a cancel button with default title "Cancel" + // /// which can be customized using localizedCancelTitle property, and a fallback button with + // /// default title "Enter Password" which can be customized using localizedFallbackTitle + // /// property. Fallback button is initially hidden and shows up after first unsuccessful + // /// Touch ID attempt. Tapping either button causes evaluatePolicy call to fail, returning + // /// a distinct error code: LAErrorUserCancel or LAErrorUserFallback. + // /// + // /// Face ID authentication begins with animating HUD. If it succeeds at first attempt, + // /// the HUD will disappear and no other UI is shown. If the first attempt fails, then + // /// LocalAuthentication will show a dialog with two buttons: "Cancel" and "Try Face ID Again". + // /// After second failure, the buttons are "Cancel" and "Enter Password" with the same + // // semantics as in the case of Touch ID. + // /// + // /// Biometric authentication will get locked after 5 unsuccessful attempts. After that, + // /// users have to unlock it by entering passcode. The passcode can be entered either at + // /// Lock Screen or even in app by the means of LAPolicyDeviceOwnerAuthentication. + // /// The Lock Screen unlock is preferred user experience because we generaly don't want users to + // /// enter their passcode at app's request. + // LAPolicyDeviceOwnerAuthenticationWithBiometrics NS_ENUM_AVAILABLE(10_12_2, 8_0) __WATCHOS_AVAILABLE(3.0) __TVOS_AVAILABLE(10.0) = kLAPolicyDeviceOwnerAuthenticationWithBiometrics, + // + // /// Device owner is going to be authenticated by biometry or device passcode. + // /// + // /// @discussion Biometric or passcode authentication is required. If the biometry is available, enrolled and + // /// not locked out, users are asked for it first. Otherwise they are asked to enter device + // /// passcode. If passcode is not enabled, policy evaluation will fail with LAErrorPasscodeNotSet. + // /// + // /// Biometric authentication dialog behaves similarly as the one used by + // /// LAPolicyDeviceOwnerAuthenticationWithBiometrics. However, instead of "Enter Password" + // /// button there is "Enter Passcode" button which, when tapped, switches the authentication + // /// method and allows users to enter device passcode. + // /// + // /// Passcode authentication will get locked after 6 unsuccessful attempts with progressively + // /// increased backoff delay. + // LAPolicyDeviceOwnerAuthentication NS_ENUM_AVAILABLE(10_11, 9_0) = kLAPolicyDeviceOwnerAuthentication +} diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 8c60adeb6..6e84d25d1 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1537,6 +1537,48 @@ /* Title for the 'scan QR code' view. */ "SCAN_QR_CODE_VIEW_TITLE" = "Scan QR Code"; +/* Title for alert indicating that screen lock could not be disabled. */ +"SCREEN_LOCK_DISABLE_FAILED" = "Screen Lock Could Not Be Disabled"; + +/* Title for alert indicating that screen lock could not be enabled. */ +"SCREEN_LOCK_ENABLE_FAILED" = "Screen Lock Could Not Be Enabled"; + +/* Indicates that an unknown error occurred while using Touch ID or Face ID. */ +"SCREEN_LOCK_ENABLE_UNKNOWN_ERROR" = "Touch ID and Face ID could not be accessed."; + +/* Indicates that Touch ID/Face ID authentication failed. */ +"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED" = "Authentication with Touch ID or Face ID failed."; + +/* Indicates that Touch ID/Face ID is 'locked out' on this device due to authentication failures. */ +"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT" = "Touch ID or Face ID is locked out; too many failures."; + +/* Indicates that Touch ID/Face ID are not available on this device. */ +"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE" = "Touch ID or Face ID is not available on this device."; + +/* Indicates that Touch ID/Face ID is not configured on this device. */ +"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED" = "Touch ID or Face ID is not configured."; + +/* Indicates that Touch ID/Face ID passcode is not set. */ +"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET" = "Touch ID or Face ID is not available; no passcode has been set."; + +/* Description of how and why Signal iOS uses Touch ID/Face ID to disable 'screen lock'. */ +"SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK" = "Use Touch ID or Face ID to disable Screen Lock."; + +/* Description of how and why Signal iOS uses Face ID to disable 'screen lock'. */ +"SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK_FACE_ID" = "Use Face ID to disable Screen Lock."; + +/* Description of how and why Signal iOS uses Touch ID to disable 'screen lock'. */ +"SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK_TOUCH_ID" = "Use Touch ID to disable Screen Lock."; + +/* Description of how and why Signal iOS uses Touch ID/Face ID to enable 'screen lock'. */ +"SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK" = "Use Touch ID or Face ID to lock access to Signal."; + +/* Description of how and why Signal iOS uses Face ID to enable 'screen lock'. */ +"SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK_FACE_ID" = "Use Face ID to lock access to Signal."; + +/* Description of how and why Signal iOS uses Touch ID to enable 'screen lock'. */ +"SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK_TOUCH_ID" = "Use Touch ID to lock access to Signal."; + /* No comment provided by engineer. */ "SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT" = "Search by name or number"; @@ -1747,6 +1789,15 @@ /* Remove metadata section header */ "SETTINGS_REMOVE_METADATA_TITLE" = "Metadata"; +/* Footer for the 'screen lock' section of the privacy settings. */ +"SETTINGS_SCREEN_LOCK_SECTION_FOOTER" = "Lock Signal access with iOS Touch ID or Face ID."; + +/* Title for the 'screen lock' section of the privacy settings. */ +"SETTINGS_SCREEN_LOCK_SECTION_TITLE" = "Screen Lock"; + +/* Label for the 'enable screen lock' switch of the privacy settings. */ +"SETTINGS_SCREEN_LOCK_SWITCH_LABEL" = "Screen Lock"; + /* No comment provided by engineer. */ "SETTINGS_SCREEN_SECURITY" = "Enable Screen Security"; diff --git a/SignalServiceKit/src/Util/OWSError.h b/SignalServiceKit/src/Util/OWSError.h index 0a06d88ca..b5c326a9f 100644 --- a/SignalServiceKit/src/Util/OWSError.h +++ b/SignalServiceKit/src/Util/OWSError.h @@ -41,6 +41,7 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) { OWSErrorCodeImportBackupError = 777418, // A non-recoverable while importing or exporting a backup. OWSErrorCodeBackupFailure = 777419, + OWSErrorCodeLocalAuthenticationError = 777420, }; extern NSString *const OWSErrorRecipientIdentifierKey;