Skip to content

Commit

Permalink
Merge branch 'jens/iphone-15-disable-otp'
Browse files Browse the repository at this point in the history
  • Loading branch information
jensutbult committed Oct 30, 2023
2 parents 9627b70 + 849c58d commit cb9af95
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 30 deletions.
8 changes: 8 additions & 0 deletions Authenticator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
A5E9DEB0237DE1660011FBF4 /* SettingsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E9DEAF237DE1660011FBF4 /* SettingsConfig.swift */; };
B40327742847AB5000DF4DB0 /* LicensingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40327732847AB5000DF4DB0 /* LicensingViewController.swift */; };
B40327762847AE0A00DF4DB0 /* Licensing.md in Resources */ = {isa = PBXBuildFile; fileRef = B40327752847AE0A00DF4DB0 /* Licensing.md */; };
B40D61A02AE7F37900467AE9 /* DisableOTPView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40D619F2AE7F37900467AE9 /* DisableOTPView.swift */; };
B40D61A22AE7F89500467AE9 /* DisableOTPModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40D61A12AE7F89500467AE9 /* DisableOTPModel.swift */; };
B411242F29D423A300D58001 /* ListStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B411242E29D423A300D58001 /* ListStatusView.swift */; };
B432B1BF28B65B8600A7182F /* YubiKit in Frameworks */ = {isa = PBXBuildFile; productRef = B432B1BE28B65B8600A7182F /* YubiKit */; };
B452EC1F2A1E4F460045E5D9 /* YubiOtpRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B452EC1E2A1E4F460045E5D9 /* YubiOtpRowView.swift */; };
Expand Down Expand Up @@ -220,6 +222,8 @@
A5E9DEAF237DE1660011FBF4 /* SettingsConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsConfig.swift; sourceTree = "<group>"; };
B40327732847AB5000DF4DB0 /* LicensingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensingViewController.swift; sourceTree = "<group>"; };
B40327752847AE0A00DF4DB0 /* Licensing.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Licensing.md; sourceTree = "<group>"; };
B40D619F2AE7F37900467AE9 /* DisableOTPView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableOTPView.swift; sourceTree = "<group>"; };
B40D61A12AE7F89500467AE9 /* DisableOTPModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableOTPModel.swift; sourceTree = "<group>"; };
B411242E29D423A300D58001 /* ListStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStatusView.swift; sourceTree = "<group>"; };
B452EC1E2A1E4F460045E5D9 /* YubiOtpRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YubiOtpRowView.swift; sourceTree = "<group>"; };
B452EC3C2A264A620045E5D9 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -463,6 +467,7 @@
B452EC1E2A1E4F460045E5D9 /* YubiOtpRowView.swift */,
B4719B312993EFEE006CDAEA /* AccountDetailsView.swift */,
B4C93E5F299D156C00C2A8B8 /* ErrorAlertView.swift */,
B40D619F2AE7F37900467AE9 /* DisableOTPView.swift */,
);
path = UI;
sourceTree = "<group>";
Expand Down Expand Up @@ -493,6 +498,7 @@
B4C93E62299FB51A00C2A8B8 /* Account.swift */,
B4719B1A298AB641006CDAEA /* MainViewModel.swift */,
B4FE90D12A4431AB00B59170 /* NotificationsViewModel.swift */,
B40D61A12AE7F89500467AE9 /* DisableOTPModel.swift */,
);
path = Model;
sourceTree = "<group>";
Expand Down Expand Up @@ -705,6 +711,7 @@
816C684823430F8E00209342 /* SecureStoreQueryable.swift in Sources */,
515542882656F64100B19C59 /* Data+Extensions.swift in Sources */,
51002C2E267C95D9005D5A7C /* YubiKeyInformationViewModel.swift in Sources */,
B40D61A02AE7F37900467AE9 /* DisableOTPView.swift in Sources */,
B4FE90D22A4431AB00B59170 /* NotificationsViewModel.swift in Sources */,
B4719B17298AA6E7006CDAEA /* MainView.swift in Sources */,
B452EC442A2A06940045E5D9 /* ToastPresenter.swift in Sources */,
Expand Down Expand Up @@ -736,6 +743,7 @@
513F34C22463F44300FCE030 /* EditCredentialController.swift in Sources */,
B4C93E9129C0B70B00C2A8B8 /* ConfigurationWrapper.swift in Sources */,
513D4DF22660D6570022C53D /* AddCredentialController.swift in Sources */,
B40D61A22AE7F89500467AE9 /* DisableOTPModel.swift in Sources */,
51D1E84E26427F7600BDA3FF /* PasswordCache.swift in Sources */,
A5D4E86D24083CF300FD63A0 /* OTPConfigurationController.swift in Sources */,
5156D05F265D3CEF007A94F8 /* TokenRequestViewModel.swift in Sources */,
Expand Down
125 changes: 125 additions & 0 deletions Authenticator/Model/DisableOTPModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright (C) Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

class DisableOTPModel: ObservableObject {
private let sessionHandler = ManagementSessionHandler()

@Published var otpDisabled: Bool = false
@Published var keyRemoved: Bool = false
@Published var keyIgnored: Bool = false

init() {
sessionHandler.closingCallback = { [weak self] error in
DispatchQueue.main.async {
self?.keyRemoved = true
}
}
}

func disableOTP() {
Task { @MainActor in
guard let session = try? await self.sessionHandler.session() else { return }
guard let deviceInfo = try? await session.deviceInfo() else { return }
guard let configuration = deviceInfo.configuration else { return }
configuration.setEnabled(false, application: .OTP, overTransport: .USB)
try await session.write(configuration, reboot: false)
self.otpDisabled = true
}
}

func ignoreThisKey() {
Task { @MainActor in
guard let session = try? await self.sessionHandler.session() else { return }
guard let deviceInfo = try? await session.deviceInfo() else { return }
SettingsConfig.registerUSBCDeviceToIgnore(deviceId: deviceInfo.serialNumber)
self.keyIgnored = true
}
}
}

fileprivate class ManagementSessionHandler: NSObject, YKFManagerDelegate {

override init() {
super.init()
DelegateStack.shared.setDelegate(self)
}

deinit {
DelegateStack.shared.removeDelegate(self)
}

private var smartCardConnection: YKFSmartCardConnection?
private var currentSession: YKFManagementSession?

private var connectionCallback: ((_ connection: YKFConnectionProtocol) -> Void)?
fileprivate var closingCallback: ((_ error: Error?) -> Void)?

func didConnectSmartCard(_ connection: YKFSmartCardConnection) {
print(connection.smartCardInterface.hashValue)
smartCardConnection = connection
connectionCallback?(connection)
connectionCallback = nil
}

func didDisconnectSmartCard(_ connection: YKFSmartCardConnection, error: Error?) {
smartCardConnection = nil
closingCallback?(error)
closingCallback = nil
currentSession = nil
}

var completion: ((YKFNFCConnection) -> Void)?

func session() async throws -> YKFManagementSession {
return try await withCheckedThrowingContinuation { continuation in
guard !Task.isCancelled else {
continuation.resume(throwing: CancellationError())
return
}
if let smartCardConnection {
smartCardConnection.managementSession { session, error in
if let session {
continuation.resume(returning: session)
} else {
continuation.resume(throwing: error!)
}
}
return
}

self.completion = { connection in
connection.managementSession { session, error in
if let session {
continuation.resume(returning: session)
} else {
continuation.resume(throwing: error!)
}
self.completion = nil
}
}
}
}
}

extension ManagementSessionHandler {
// Not used but implemented to conform to YKFManagerDelegate protocol.
func didConnectNFC(_ connection: YKFNFCConnection) { }
func didDisconnectNFC(_ connection: YKFNFCConnection, error: Error?) { }
func didConnectAccessory(_ connection: YKFAccessoryConnection) { }
func didDisconnectAccessory(_ connection: YKFAccessoryConnection, error: Error?) { }
}
41 changes: 28 additions & 13 deletions Authenticator/Model/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class MainViewModel: ObservableObject {
@Published var accountsLoaded: Bool = false
@Published var presentPasswordEntry: Bool = false
@Published var presentPasswordSaveType: Bool = false
@Published var presentDisableOTP: Bool = false
@Published var passwordEntryMessage: String = ""
@Published var isKeyPluggedIn: Bool = false
@Published var error: Error?
Expand Down Expand Up @@ -84,19 +85,30 @@ class MainViewModel: ObservableObject {

@MainActor func start() {
sessionTask = Task { [weak self] in
for await session in OATHSessionHandler.shared.wiredSessions() {
self?.isKeyPluggedIn = true
await self?.updateAccounts(using: session)
let error = await session.sessionDidEnd()
await MainActor.run { [weak self] in
self?.favoritesCancellables.forEach { $0.cancel() }
self?.favoritesCancellables.removeAll()
self?.accounts.removeAll()
self?.pinnedAccounts.removeAll()
self?.otherAccounts.removeAll()
self?.accountsLoaded = false
self?.isKeyPluggedIn = false
self?.error = error
do {
for try await session in OATHSessionHandler.shared.wiredSessions() {
self?.isKeyPluggedIn = true
await self?.updateAccounts(using: session)
let error = await session.sessionDidEnd()
await MainActor.run { [weak self] in
self?.favoritesCancellables.forEach { $0.cancel() }
self?.favoritesCancellables.removeAll()
self?.accounts.forEach { account in
account.invalidate()
}
self?.accounts.removeAll()
self?.pinnedAccounts.removeAll()
self?.otherAccounts.removeAll()
self?.accountsLoaded = false
self?.isKeyPluggedIn = false
self?.error = error
}
}
} catch {
// Only handle .otpEnabledError by presenting the disable OTP modal
if let sessionError = error as? OATHSessionError, sessionError == .otpEnabledError {
self?.sessionTask?.cancel()
self?.presentDisableOTP = true
}
}
}
Expand All @@ -105,6 +117,9 @@ class MainViewModel: ObservableObject {
@MainActor func stop() {
sessionTask?.cancel()
sessionTask = nil
accounts.forEach { account in
account.invalidate()
}
accounts.removeAll()
pinnedAccounts.removeAll()
otherAccounts.removeAll()
Expand Down
55 changes: 44 additions & 11 deletions Authenticator/Model/OATHSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@

import Foundation

enum OATHSessionError: Error, LocalizedError {
enum OATHSessionError: Error, LocalizedError, Equatable {

case credentialAlreadyPresent(YKFOATHCredentialTemplate);
case otpEnabledError;

public var errorDescription: String? {
switch self {
case .credentialAlreadyPresent(let credential):
return "There's already an account named \(credential.issuer.isEmpty == false ? "\(credential.issuer), \(credential.accountName)" : credential.accountName) on this YubiKey."
case .otpEnabledError:
return "Yubico OTP enabled"
}
}
}
Expand Down Expand Up @@ -83,11 +87,13 @@ class OATHSessionHandler: NSObject, YKFManagerDelegate {

struct WiredOATHSessions: AsyncSequence {
typealias Element = OATHSession
var current: OATHSession? = nil
struct AsyncIterator: AsyncIteratorProtocol {
mutating func next() async -> Element? {
mutating func next() async throws -> Element? {
guard !Task.isCancelled else {
return nil
}
while true {
return try? await OATHSessionHandler.shared.newWiredSession()
return try await OATHSessionHandler.shared.newWiredSession()
}
}
}
Expand Down Expand Up @@ -140,17 +146,44 @@ class OATHSessionHandler: NSObject, YKFManagerDelegate {
YubiKitManager.shared.startSmartCardConnection()
}
return try await withTaskCancellationHandler {
let deviceType = await UIDevice.current.userInterfaceIdiom
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<OATHSession, Error>) in
self.wiredContinuation = continuation
self.wiredConnectionCallback = { connection in
connection.oathSession { session, error in
if let session {
self.currentSession = session
continuation.resume(returning: OATHSession(session: session, type: .wired))
} else {
continuation.resume(throwing: error!)
if connection.isKind(of: YKFSmartCardConnection.self) && deviceType == .phone {
connection.managementSession { session, error in
guard let session else { continuation.resume(throwing: error!); return }
session.getDeviceInfo { deviceInfo, error in
guard let deviceInfo else { continuation.resume(throwing: error!); return }
guard let configuration = deviceInfo.configuration else { continuation.resume(throwing: "Error!!!"); return }
guard !configuration.isEnabled(.OTP, overTransport: .USB) || SettingsConfig.isOTPOverUSBIgnored(deviceId: deviceInfo.serialNumber) else {
continuation.resume(throwing: OATHSessionError.otpEnabledError)
self.wiredContinuation = nil
self.wiredConnectionCallback = nil
return
}
connection.oathSession { session, error in
if let session {
self.currentSession = session
continuation.resume(returning: OATHSession(session: session, type: .wired))
} else {
continuation.resume(throwing: error!)
}
self.wiredContinuation = nil
self.wiredConnectionCallback = nil
return
}
}
}
} else {
connection.oathSession { session, error in
if let session {
self.currentSession = session
continuation.resume(returning: OATHSession(session: session, type: .wired))
} else {
continuation.resume(throwing: error!)
}
}
self.wiredContinuation = nil
}
}
if let connection: YKFConnectionProtocol = self.accessoryConnection ?? self.smartCardConnection {
Expand Down
15 changes: 14 additions & 1 deletion Authenticator/Model/SettingsConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class SettingsConfig {
static private let showWhatsNewCounterAppVersion = "showWhatsNewCounterAppVersion"
static private let nfcOnOTPLaunch = "nfcOnOTPLaunch"
static private let copyOTP = "copyOTP"
static private let ignoreUSBCWithOTP = "ignoreUSBCWithOTP"

static var showWhatsNewText: Bool {
get {
Expand Down Expand Up @@ -75,7 +76,7 @@ class SettingsConfig {
UserDefaults.standard.set(newValue, forKey: userFoundMenu)
}
}

static var showNoServiceWarning: Bool {
get {
return UserDefaults.standard.bool(forKey: noServiceWarning)
Expand Down Expand Up @@ -151,4 +152,16 @@ class SettingsConfig {
UserDefaults.standard.set(newValue, forKey: copyOTP)
}
}

static func isOTPOverUSBIgnored(deviceId: UInt) -> Bool {
guard let list = UserDefaults.standard.array(forKey: ignoreUSBCWithOTP) as? [UInt] else { return false }
return list.contains(deviceId)
}

static func registerUSBCDeviceToIgnore(deviceId: UInt) {
let list = UserDefaults.standard.array(forKey: ignoreUSBCWithOTP) as? [UInt] ?? [UInt]()
var set = Set(list)
set.insert(deviceId)
UserDefaults.standard.set(Array(set), forKey: ignoreUSBCWithOTP)
}
}
Loading

0 comments on commit cb9af95

Please sign in to comment.