Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show modal with option to disable OTP on iPhone 15 with USB-C #133

Merged
merged 8 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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