Skip to content

Commit

Permalink
Adds support for reading Yubico OTP (#122)
Browse files Browse the repository at this point in the history
* Added settings that will open app and read OATH accounts when an NDEF OTP Tag has been read.

* Use iPhone 11 instead of 13.

* Fixe compilation error only visible on github.

* Show Yubico OTP in table.

* Default setting for "Start NFC on OTP tag read" is now true.

* Added dummy item for changes in 1.7.4 to stop "Whats new" modal from being displayed on every app start.

* Better parsing of OTP URLs. Copy OTP to clipboard setting now does what is expected. Visual tweaks to OTP table view cell.

* OTP is now displayed in table even if NFC is not activated.

* Proper dark mode color of OTP icon. Updated copy for NFC settings.

* Updated changelog for 1.7.4.

* Fixes issue where "Initiate NFC at application start" and "Activate NFC on OTP tag read" where both enabled at the same time.
  • Loading branch information
jensutbult authored May 24, 2023
1 parent c936cbd commit 38230ec
Show file tree
Hide file tree
Showing 14 changed files with 438 additions and 169 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Test on emulator
run: xcodebuild clean test -disablePackageRepositoryCache -sdk iphonesimulator -project Authenticator.xcodeproj -scheme Authenticator -destination "platform=iOS Simulator,OS=latest,name=iPhone 13" | xcpretty --test --color && exit ${PIPESTATUS[0]}
run: xcodebuild clean test -disablePackageRepositoryCache -sdk iphonesimulator -project Authenticator.xcodeproj -scheme Authenticator -destination "platform=iOS Simulator,OS=latest,name=iPhone 11" | xcpretty --test --color && exit ${PIPESTATUS[0]}
28 changes: 16 additions & 12 deletions Authenticator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
5180974326DE185100A122C1 /* ResetOATHViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180974226DE185100A122C1 /* ResetOATHViewModel.swift */; };
51A162862678A1F100C3FA1E /* OATHConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A162852678A1F100C3FA1E /* OATHConfigurationController.swift */; };
51AFD4D227103E36008F2630 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AFD4D127103E36008F2630 /* SearchBar.swift */; };
51AFD4D42716FC78008F2630 /* ApplicationSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AFD4D32716FC78008F2630 /* ApplicationSettingsController.swift */; };
51AFD4D42716FC78008F2630 /* NFCSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AFD4D32716FC78008F2630 /* NFCSettingsController.swift */; };
51AFD4D62716FCDB008F2630 /* ApplicationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AFD4D52716FCDB008F2630 /* ApplicationSettingsViewModel.swift */; };
51AFD4D827196AB6008F2630 /* VersionHistory.plist in Resources */ = {isa = PBXBuildFile; fileRef = 51AFD4D727196AB6008F2630 /* VersionHistory.plist */; };
51AFD4DA271D4278008F2630 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51AFD4D9271D4277008F2630 /* QuartzCore.framework */; };
Expand Down Expand Up @@ -91,6 +91,7 @@
B432B1BF28B65B8600A7182F /* YubiKit in Frameworks */ = {isa = PBXBuildFile; productRef = B432B1BE28B65B8600A7182F /* YubiKit */; };
B4712B7028DDB5F6009B270D /* AccessKeyCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4712B6F28DDB5F6009B270D /* AccessKeyCache.swift */; };
B4B1711827DF8C48002A62DE /* ScanAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1711727DF8C48002A62DE /* ScanAccountView.swift */; };
B4EE055F2A0CDB3C002F30D4 /* OTPTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EE055E2A0CDB3C002F30D4 /* OTPTableViewCell.swift */; };
B9F0FF11F842A39183974083 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -163,7 +164,7 @@
5180974226DE185100A122C1 /* ResetOATHViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetOATHViewModel.swift; sourceTree = "<group>"; };
51A162852678A1F100C3FA1E /* OATHConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATHConfigurationController.swift; sourceTree = "<group>"; };
51AFD4D127103E36008F2630 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
51AFD4D32716FC78008F2630 /* ApplicationSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingsController.swift; sourceTree = "<group>"; };
51AFD4D32716FC78008F2630 /* NFCSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCSettingsController.swift; sourceTree = "<group>"; };
51AFD4D52716FCDB008F2630 /* ApplicationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingsViewModel.swift; sourceTree = "<group>"; };
51AFD4D727196AB6008F2630 /* VersionHistory.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = VersionHistory.plist; sourceTree = "<group>"; };
51AFD4D9271D4277008F2630 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -220,6 +221,7 @@
B4712B6F28DDB5F6009B270D /* AccessKeyCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessKeyCache.swift; sourceTree = "<group>"; };
B4B1711727DF8C48002A62DE /* ScanAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanAccountView.swift; sourceTree = "<group>"; };
B4E9D46228B65B5100D51FFC /* yubikit-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "yubikit-ios"; path = "../yubikit-ios"; sourceTree = "<group>"; };
B4EE055E2A0CDB3C002F30D4 /* OTPTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPTableViewCell.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -258,6 +260,7 @@
818866F722E8E19D006BC0A8 /* CredentialTableViewCell.swift */,
51E9BE3926D9038C00F9E2FC /* OATHCodeDetailsView.swift */,
818866F922E96B47006BC0A8 /* PieProgressBar.swift */,
B4EE055E2A0CDB3C002F30D4 /* OTPTableViewCell.swift */,
513F34BC24633A4A00FCE030 /* EditCredential */,
81FA3C2B2319839B009C22AB /* AddCredential */,
);
Expand All @@ -272,7 +275,7 @@
A5D4E86C24083CF300FD63A0 /* OTPConfigurationController.swift */,
515542672654413600B19C59 /* SmartCardConfigurationController.swift */,
81FA3C31231AF289009C22AB /* SetPasswordViewController.swift */,
51AFD4D32716FC78008F2630 /* ApplicationSettingsController.swift */,
51AFD4D32716FC78008F2630 /* NFCSettingsController.swift */,
);
path = YubiKeyConfiguration;
sourceTree = "<group>";
Expand Down Expand Up @@ -684,6 +687,7 @@
B40327742847AB5000DF4DB0 /* LicensingViewController.swift in Sources */,
51AFD4D227103E36008F2630 /* SearchBar.swift in Sources */,
811CD95722FB276A00E2BCBB /* HelpViewController.swift in Sources */,
B4EE055F2A0CDB3C002F30D4 /* OTPTableViewCell.swift in Sources */,
81C00FDB22EB70A500C54903 /* OATHViewModel.swift in Sources */,
818866E922E18134006BC0A8 /* OATHViewController.swift in Sources */,
51D1E84C264134F300BDA3FF /* UIAlertController+Extensions.swift in Sources */,
Expand All @@ -703,7 +707,7 @@
515542682654413600B19C59 /* SmartCardConfigurationController.swift in Sources */,
5155428326569ADD00B19C59 /* UIControl+Extensions.swift in Sources */,
A544948F23CE546B003E1E07 /* TutorialPagesViewControllers.swift in Sources */,
51AFD4D42716FC78008F2630 /* ApplicationSettingsController.swift in Sources */,
51AFD4D42716FC78008F2630 /* NFCSettingsController.swift in Sources */,
51394C5C26D4D460009F366D /* MenuGestureRecognizer.swift in Sources */,
A5588BF9239B0A7F003E4CA5 /* FavoritesStorage.swift in Sources */,
B4B1711827DF8C48002A62DE /* ScanAccountView.swift in Sources */,
Expand Down Expand Up @@ -779,7 +783,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = TokenExtension/TokenExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 107;
CURRENT_PROJECT_VERSION = 110;
DEVELOPMENT_TEAM = LQA3CS5MM7;
INFOPLIST_FILE = TokenExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
Expand All @@ -788,7 +792,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.3;
MARKETING_VERSION = 1.7.4;
PRODUCT_BUNDLE_IDENTIFIER = com.yubico.Authenticator.TokenExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -802,7 +806,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = TokenExtension/TokenExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 107;
CURRENT_PROJECT_VERSION = 110;
DEVELOPMENT_TEAM = LQA3CS5MM7;
INFOPLIST_FILE = TokenExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
Expand All @@ -811,7 +815,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.3;
MARKETING_VERSION = 1.7.4;
PRODUCT_BUNDLE_IDENTIFIER = com.yubico.Authenticator.TokenExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -945,7 +949,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Authenticator/Authenticator.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 107;
CURRENT_PROJECT_VERSION = 110;
DEVELOPMENT_TEAM = LQA3CS5MM7;
HEADER_SEARCH_PATHS = "../Submodules/YubiKit/**";
INFOPLIST_FILE = Authenticator/Info.plist;
Expand All @@ -955,7 +959,7 @@
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "";
MARKETING_VERSION = 1.7.3;
MARKETING_VERSION = 1.7.4;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.yubico.Authenticator;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -973,7 +977,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Authenticator/Authenticator.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 107;
CURRENT_PROJECT_VERSION = 110;
DEVELOPMENT_TEAM = LQA3CS5MM7;
HEADER_SEARCH_PATHS = "../Submodules/YubiKit/**";
INFOPLIST_FILE = Authenticator/Info.plist;
Expand All @@ -983,7 +987,7 @@
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "";
MARKETING_VERSION = 1.7.3;
MARKETING_VERSION = 1.7.4;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_BUNDLE_IDENTIFIER = com.yubico.Authenticator;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
27 changes: 27 additions & 0 deletions Authenticator/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
coder.encode(1.6, forKey: "AppVersion")
return true
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {

if let main = UIApplication.shared.windows.first?.rootViewController?.children.first as? OATHViewController {
if let url = userActivity.webpageURL {
var otp: String
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let fragment = components?.fragment {
otp = fragment
} else {
otp = url.lastPathComponent
}
main.dismiss(animated: false) {
main.otp = otp
}
}

if SettingsConfig.isNFCOnOTPLaunchEnabled {
main.dismiss(animated: false) {
main.refreshData()
}
}
}

return true
}

func application(_ application: UIApplication, shouldRestoreSecureApplicationState coder: NSCoder) -> Bool {
let version = coder.decodeFloat(forKey: "AppVersion")
Expand Down
4 changes: 4 additions & 0 deletions Authenticator/Authenticator.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:my.yubico.com</string>
</array>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
Expand Down
18 changes: 18 additions & 0 deletions Authenticator/Model/ApplicationSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,22 @@ struct ApplicationSettingsViewModel {
SettingsConfig.isNFCOnAppLaunchEnabled = newValue
}
}

var isNFCOnOTPLaunchEnabled: Bool {
get {
return SettingsConfig.isNFCOnOTPLaunchEnabled
}
set {
SettingsConfig.isNFCOnOTPLaunchEnabled = newValue
}
}

var isCopyOTPEnabled: Bool {
get {
return SettingsConfig.isCopyOTPEnabled
}
set {
SettingsConfig.isCopyOTPEnabled = newValue
}
}
}
23 changes: 15 additions & 8 deletions Authenticator/Model/OATHViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,32 +52,39 @@ class OATHViewModel: NSObject, YKFManagerDelegate {

private var nfcConnection: YKFNFCConnection?

private var lastNFCEndingTimestamp: Date?

private var lastNFCStartTimestamp: Date?
private var lastNFCEndTimestamp: Date?

var accessKeyMemoryCache = AccessKeyCache()
let accessKeySecureStore = SecureStore(secureStoreQueryable: PasswordQueryable(service: "OATH"))
let passwordPreferences = PasswordPreferences()

var didNFCEndRecently: Bool {
guard let ts = lastNFCEndingTimestamp else { return false }
guard let ts = lastNFCEndTimestamp else { return false }
return ts.addingTimeInterval(5) > Date()
}

var didNFCStartRecently: Bool {
guard let ts = lastNFCStartTimestamp else { return false }
return ts.addingTimeInterval(2) > Date()
}

func didConnectNFC(_ connection: YKFNFCConnection) {
nfcConnection = connection
lastNFCStartTimestamp = Date()
if let callback = connectionCallback {
callback(connection)
}
}

func didDisconnectNFC(_ connection: YKFNFCConnection, error: Error?) {
lastNFCEndingTimestamp = Date()
lastNFCEndTimestamp = Date()
nfcConnection = nil
session = nil
}

func didFailConnectingNFC(_ error: Error) {
lastNFCEndingTimestamp = Date()
lastNFCEndTimestamp = Date()
}

private var accessoryConnection: YKFAccessoryConnection?
Expand Down Expand Up @@ -545,10 +552,10 @@ class OATHViewModel: NSObject, YKFManagerDelegate {
self.delegate?.onOperationCompleted(operation: .filter)
}

public func copyToClipboard(credential: Credential) {
public func copyToClipboard(value: String, message: String = "Copied to clipboard") {
// copy to clipbboard
UIPasteboard.general.string = credential.code
self.delegate?.onShowToastMessage(message: "Copied to clipboard")
UIPasteboard.general.string = value
self.delegate?.onShowToastMessage(message: message)
}

public func emulateSomeRecords() {
Expand Down
25 changes: 24 additions & 1 deletion Authenticator/Model/SettingsConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class SettingsConfig {
static private let showNFCSwipeHintCounter = "showNFCSwipeHintCounter"
static private let showWhatsNewCounter = "showWhatsNewCounter"
static private let showWhatsNewCounterAppVersion = "showWhatsNewCounterAppVersion"

static private let nfcOnOTPLaunch = "nfcOnOTPLaunch"
static private let copyOTP = "copyOTP"

static var showWhatsNewText: Bool {
get {
Expand Down Expand Up @@ -128,4 +129,26 @@ class SettingsConfig {
UserDefaults.standard.set(newValue, forKey: nfcOnAppLaunch)
}
}

static var isNFCOnOTPLaunchEnabled: Bool {
get {
if UserDefaults.standard.object(forKey: nfcOnOTPLaunch) == nil {
UserDefaults.standard.set(true, forKey: nfcOnOTPLaunch)
}

return UserDefaults.standard.bool(forKey: nfcOnOTPLaunch)
}
set {
UserDefaults.standard.set(newValue, forKey: nfcOnOTPLaunch)
}
}

static var isCopyOTPEnabled: Bool {
get {
return UserDefaults.standard.bool(forKey: copyOTP)
}
set {
UserDefaults.standard.set(newValue, forKey: copyOTP)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ class AddCredentialController: UITableViewController {
case .account(let account):
self.credential = account
self.mode = .prefilled
updateWith(credential: account)
self.updateWith(credential: account)
}
}
self.scanAccountView = scanAccountView
Expand Down
2 changes: 1 addition & 1 deletion Authenticator/UI/Authentication/OATHCodeDetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class OATHCodeDetailsView: UIVisualEffectView {
copyMenuAction = {
MenuAction(title: "Copy", image: UIImage(systemName: "square.and.arrow.up"),
action: {
viewModel.copyToClipboard(credential: credential)
viewModel.copyToClipboard(value: credential.code)
})
}()

Expand Down
Loading

0 comments on commit 38230ec

Please sign in to comment.