mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Ensure that the current user is the device owner before linking a new device. (#5773)
This commit is contained in:
@@ -17,6 +17,7 @@ class AppLockService: AppLockServiceProtocol {
|
||||
|
||||
private let timer: AppLockTimer
|
||||
private let unlockPolicy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics
|
||||
private let deviceOwnerPolicy: LAPolicy = .deviceOwnerAuthentication
|
||||
|
||||
var isMandatory: Bool {
|
||||
appSettings.appLockIsMandatory
|
||||
@@ -163,6 +164,30 @@ class AppLockService: AppLockServiceProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func verifyDeviceOwner(reason: String) async -> AppLockDeviceOwnerResult {
|
||||
let context = verificationContext()
|
||||
|
||||
var error: NSError?
|
||||
guard context.canEvaluatePolicy(deviceOwnerPolicy, error: &error) else {
|
||||
MXLog.warning("Device owner verification unavailable: \(String(describing: error))")
|
||||
return isEnabled ? .appLockPINRequired : .unavailable
|
||||
}
|
||||
|
||||
do {
|
||||
guard try await context.evaluatePolicy(deviceOwnerPolicy, localizedReason: reason) else {
|
||||
MXLog.warning("Device owner verification completed without success.")
|
||||
return .unverified
|
||||
}
|
||||
return .verified
|
||||
} catch LAError.userCancel, LAError.systemCancel {
|
||||
MXLog.info("Device owner verification was cancelled.")
|
||||
return .cancelled
|
||||
} catch {
|
||||
MXLog.warning("Device owner verification failed: \(error)")
|
||||
return .error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Queries the context for supported biometrics and enrolment state.
|
||||
@@ -187,6 +212,12 @@ class AppLockService: AppLockServiceProtocol {
|
||||
return context
|
||||
}
|
||||
|
||||
/// Creates a fresh context for device owner verification so the user is always prompted.
|
||||
private func verificationContext() -> LAContext {
|
||||
// Keep using the injected context for tests etc.
|
||||
type(of: context) == LAContext.self ? LAContext() : context
|
||||
}
|
||||
|
||||
/// Shared logic for completing an unlock via a PIN or biometrics.
|
||||
private func completeUnlock() {
|
||||
timer.registerUnlock()
|
||||
|
||||
@@ -34,6 +34,22 @@ enum AppLockServiceBiometricResult {
|
||||
case interrupted
|
||||
}
|
||||
|
||||
/// The result of an attempt to verify that the user is the device owner.
|
||||
enum AppLockDeviceOwnerResult {
|
||||
/// The device owner was successfully verified.
|
||||
case verified
|
||||
/// The verification completed without verifying the user.
|
||||
case unverified
|
||||
/// Verification isn't available because a device passcode isn't set.
|
||||
case unavailable
|
||||
/// Verification isn't available, but an App Lock PIN is set and can be used instead.
|
||||
case appLockPINRequired
|
||||
/// The verification attempt was cancelled.
|
||||
case cancelled
|
||||
/// The verification couldn't be completed due to an error.
|
||||
case error
|
||||
}
|
||||
|
||||
protocol AppLockServiceProtocol: AnyObject {
|
||||
/// The use of a PIN code is mandatory for this device.
|
||||
var isMandatory: Bool { get }
|
||||
@@ -72,6 +88,13 @@ protocol AppLockServiceProtocol: AnyObject {
|
||||
/// Attempt to unlock the app using FaceID or TouchID.
|
||||
func unlockWithBiometrics() async -> AppLockServiceBiometricResult
|
||||
|
||||
/// Verifies the device owner via the device passcode/biometrics, e.g. to gate a sensitive action.
|
||||
///
|
||||
/// This is a standalone identity check: it does **not** unlock the app or touch any App Lock
|
||||
/// state (the grace-period timer or PIN attempt count).
|
||||
/// - Parameter reason: The localised reason shown in the system prompt.
|
||||
func verifyDeviceOwner(reason: String) async -> AppLockDeviceOwnerResult
|
||||
|
||||
/// The number of attempts the user had made to unlock with a PIN code.
|
||||
///
|
||||
/// Note: We don't track the biometric attempts as LAContext does that automatically.
|
||||
|
||||
@@ -15,7 +15,7 @@ final nonisolated class AppLockTimer {
|
||||
|
||||
/// Whether the timer considers the app to be locked or not. Always starts with a locked app.
|
||||
///
|
||||
/// Internally this value may be incorrect, always call `needsUnlock` to get the correct value.
|
||||
/// Internally this value may be incorrect, always call ``computeLockState`` to get the correct value.
|
||||
private var isLocked = true
|
||||
/// The date when the app was last backgrounded whilst in an unlocked state.
|
||||
private var lastUnlockedBackground: Date?
|
||||
|
||||
@@ -25,15 +25,26 @@ nonisolated class LAContextMock: LAContext {
|
||||
internalEvaluatedPolicyDomainStateValue
|
||||
}
|
||||
|
||||
var canEvaluatePolicyReturnValue: Bool?
|
||||
override func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool {
|
||||
let result = super.canEvaluatePolicy(policy, error: error)
|
||||
let result = canEvaluatePolicyReturnValue ?? super.canEvaluatePolicy(policy, error: error)
|
||||
updateInternalValues()
|
||||
return result
|
||||
}
|
||||
|
||||
var evaluatePolicyReturnValue: Bool!
|
||||
var evaluatePolicyThrowableError: Error?
|
||||
var evaluatePolicyCallsCount = 0
|
||||
var evaluatePolicyCalled: Bool {
|
||||
evaluatePolicyCallsCount > 0
|
||||
}
|
||||
|
||||
override func evaluatePolicy(_ policy: LAPolicy, localizedReason: String) async throws -> Bool {
|
||||
evaluatePolicyCallsCount += 1
|
||||
updateInternalValues()
|
||||
if let evaluatePolicyThrowableError {
|
||||
throw evaluatePolicyThrowableError
|
||||
}
|
||||
return evaluatePolicyReturnValue
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user