diff --git a/pass/Controllers/PasswordNavigationViewController.swift b/pass/Controllers/PasswordNavigationViewController.swift index 2019c60..a46ff1f 100644 --- a/pass/Controllers/PasswordNavigationViewController.swift +++ b/pass/Controllers/PasswordNavigationViewController.swift @@ -9,6 +9,7 @@ import passKit import SVProgressHUD import UIKit +import UserNotifications extension UIStoryboard { static var passwordNavigationViewController: PasswordNavigationViewController { @@ -75,6 +76,12 @@ class PasswordNavigationViewController: UIViewController { configureTableView(in: parentPasswordEntity) configureNotification() configureSearchBar() + requestNotificationPermission() + } + + private func requestNotificationPermission() { + let permissionOptions = UNAuthorizationOptions(arrayLiteral: .alert) + UNUserNotificationCenter.current().requestAuthorization(options: permissionOptions) { _, _ in } } override func viewWillAppear(_ animated: Bool) { diff --git a/pass/Services/PasswordManager.swift b/pass/Services/PasswordManager.swift index 2650495..27c8fae 100644 --- a/pass/Services/PasswordManager.swift +++ b/pass/Services/PasswordManager.swift @@ -11,16 +11,13 @@ import SVProgressHUD import UIKit class PasswordManager { - weak var viewController: UIViewController? + private let viewController: UIViewController init(viewController: UIViewController) { self.viewController = viewController } func providePasswordPasteboard(with passwordPath: String) { - guard let viewController = viewController else { - return - } decryptPassword(in: viewController, with: passwordPath) { password in SecurePasteboard.shared.copy(textToCopy: password.password) SVProgressHUD.setDefaultMaskType(.black) @@ -31,10 +28,6 @@ class PasswordManager { } func addPassword(with password: Password) { - guard let viewController = viewController else { - return - } - encryptPassword(in: viewController, with: password) { SVProgressHUD.setDefaultMaskType(.black) SVProgressHUD.setDefaultStyle(.light) diff --git a/pass/de.lproj/Localizable.strings b/pass/de.lproj/Localizable.strings index 8cb12a9..333816f 100644 --- a/pass/de.lproj/Localizable.strings +++ b/pass/de.lproj/Localizable.strings @@ -22,6 +22,7 @@ "HmacBased" = "HMAC-basiert"; "None" = "Kein valides Token"; "ExpiresIn" = "(läuft in %ds ab)"; +"OTPFor" = "Einmalpasswort für %@"; // General (error) messages "Error" = "Fehler"; diff --git a/pass/en.lproj/Localizable.strings b/pass/en.lproj/Localizable.strings index 6cad093..fbf87ff 100644 --- a/pass/en.lproj/Localizable.strings +++ b/pass/en.lproj/Localizable.strings @@ -22,6 +22,7 @@ "HmacBased" = "HMAC-based"; "None" = "None"; "ExpiresIn" = "(expires in %ds)"; +"OTPFor" = "One-time Password for %@"; // General (error) messages "Error" = "Error"; diff --git a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift index 9aa2e9c..8a26416 100644 --- a/passAutoFillExtension/Controllers/CredentialProviderViewController.swift +++ b/passAutoFillExtension/Controllers/CredentialProviderViewController.swift @@ -19,7 +19,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { }() private lazy var credentialProvider: CredentialProvider = { [unowned self] in - CredentialProvider(viewController: self, extensionContext: extensionContext) + CredentialProvider(viewController: self, extensionContext: extensionContext, afterDecryption: Utils.showNotificationWithOTP) }() private lazy var passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false) diff --git a/passAutoFillExtension/Services/CredentialProvider.swift b/passAutoFillExtension/Services/CredentialProvider.swift index 26b7dd9..bd71bbd 100644 --- a/passAutoFillExtension/Services/CredentialProvider.swift +++ b/passAutoFillExtension/Services/CredentialProvider.swift @@ -10,13 +10,16 @@ import AuthenticationServices import passKit class CredentialProvider { - var identifier: ASCredentialServiceIdentifier? - weak var extensionContext: ASCredentialProviderExtensionContext? - weak var viewController: UIViewController? + private let viewController: UIViewController + private let extensionContext: ASCredentialProviderExtensionContext + private let afterDecryption: (Password) -> Void - init(viewController: UIViewController, extensionContext: ASCredentialProviderExtensionContext) { + var identifier: ASCredentialServiceIdentifier? + + init(viewController: UIViewController, extensionContext: ASCredentialProviderExtensionContext, afterDecryption: @escaping (Password) -> Void) { self.viewController = viewController self.extensionContext = extensionContext + self.afterDecryption = afterDecryption } func credentials(for identity: ASPasswordCredentialIdentity) { @@ -24,50 +27,35 @@ class CredentialProvider { return } - provideCredentials(in: viewController, with: recordIdentifier) { credential in - guard let credential = credential else { - return - } - self.extensionContext?.completeRequest(withSelectedCredential: credential) + decryptPassword(in: viewController, with: recordIdentifier) { password in + self.extensionContext.completeRequest(withSelectedCredential: .from(password)) + self.afterDecryption(password) } } func persistAndProvideCredentials(with passwordPath: String) { - provideCredentials(in: viewController, with: passwordPath) { credential in - guard let credential = credential else { - return - } - guard let credentialIdentity = provideCredentialIdentity(for: self.identifier, user: credential.user, recordIdentifier: passwordPath) else { - self.extensionContext?.completeRequest(withSelectedCredential: credential) - return - } - - let store = ASCredentialIdentityStore.shared - store.getState { state in - if state.isEnabled { + decryptPassword(in: viewController, with: passwordPath) { password in + if let identifier = self.identifier { + ASCredentialIdentityStore.shared.getState { state in + guard state.isEnabled else { + return + } + let credentialIdentity = ASPasswordCredentialIdentity( + serviceIdentifier: identifier, + user: password.getUsernameForCompletion(), + recordIdentifier: passwordPath + ) ASCredentialIdentityStore.shared.saveCredentialIdentities([credentialIdentity]) } } - self.extensionContext?.completeRequest(withSelectedCredential: credential) + self.extensionContext.completeRequest(withSelectedCredential: .from(password)) + self.afterDecryption(password) } } } -private func provideCredentialIdentity(for identifier: ASCredentialServiceIdentifier?, user: String, recordIdentifier: String?) -> ASPasswordCredentialIdentity? { - guard let serviceIdentifier = identifier else { - return nil - } - return ASPasswordCredentialIdentity(serviceIdentifier: serviceIdentifier, user: user, recordIdentifier: recordIdentifier) -} - -private func provideCredentials(in viewController: UIViewController?, with path: String, completion: @escaping ((ASPasswordCredential?) -> Void)) { - guard let viewController = viewController else { - return - } - decryptPassword(in: viewController, with: path) { password in - let username = password.getUsernameForCompletion() - let password = password.password - let credential = ASPasswordCredential(user: username, password: password) - completion(credential) +extension ASPasswordCredential { + static func from(_ password: Password) -> ASPasswordCredential { + ASPasswordCredential(user: password.getUsernameForCompletion(), password: password.password) } } diff --git a/passExtension/Controllers/ExtensionViewController.swift b/passExtension/Controllers/ExtensionViewController.swift index 7eb190d..f2f6cb7 100644 --- a/passExtension/Controllers/ExtensionViewController.swift +++ b/passExtension/Controllers/ExtensionViewController.swift @@ -20,7 +20,7 @@ class ExtensionViewController: UIViewController { }() private lazy var credentialProvider: CredentialProvider = { [unowned self] in - CredentialProvider(viewController: self, extensionContext: extensionContext!) + CredentialProvider(viewController: self, extensionContext: extensionContext!, afterDecryption: Utils.showNotificationWithOTP) }() private lazy var passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false) diff --git a/passExtension/Services/CredentialProvider.swift b/passExtension/Services/CredentialProvider.swift index 0365b17..a7b30b5 100644 --- a/passExtension/Services/CredentialProvider.swift +++ b/passExtension/Services/CredentialProvider.swift @@ -11,22 +11,17 @@ import passKit import UIKit class CredentialProvider { - weak var extensionContext: NSExtensionContext? - weak var viewController: UIViewController? + private let viewController: UIViewController + private let extensionContext: NSExtensionContext + private let afterDecryption: (Password) -> Void - init(viewController: UIViewController, extensionContext: NSExtensionContext) { + init(viewController: UIViewController, extensionContext: NSExtensionContext, afterDecryption: @escaping (Password) -> Void) { self.viewController = viewController self.extensionContext = extensionContext + self.afterDecryption = afterDecryption } func provideCredentialsFindLogin(with passwordPath: String) { - guard let viewController = viewController else { - return - } - guard let extensionContext = extensionContext else { - return - } - decryptPassword(in: viewController, with: passwordPath) { password in let extensionItem = NSExtensionItem() var returnDictionary = [ @@ -37,30 +32,25 @@ class CredentialProvider { returnDictionary[PassExtensionKey.totpKey] = totpPassword } extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))] - extensionContext.completeRequest(returningItems: [extensionItem]) + self.extensionContext.completeRequest(returningItems: [extensionItem]) + self.afterDecryption(password) } } func provideCredentialsBrowser(with passwordPath: String) { - guard let viewController = viewController else { - return - } - guard let extensionContext = extensionContext else { - return - } - decryptPassword(in: viewController, with: passwordPath) { password in Utils.copyToPasteboard(textToCopy: password.password) // return a dictionary for JavaScript for best-effor fill in let extensionItem = NSExtensionItem() let returnDictionary = [ NSExtensionJavaScriptFinalizeArgumentKey: [ - "username": password.getUsernameForCompletion(), - "password": password.password, + PassExtensionKey.usernameKey: password.getUsernameForCompletion(), + PassExtensionKey.passwordKey: password.password, ], ] extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))] - extensionContext.completeRequest(returningItems: [extensionItem]) + self.extensionContext.completeRequest(returningItems: [extensionItem]) + self.afterDecryption(password) } } } diff --git a/passKit/Helpers/Utils.swift b/passKit/Helpers/Utils.swift index f078590..16e854a 100644 --- a/passKit/Helpers/Utils.swift +++ b/passKit/Helpers/Utils.swift @@ -70,4 +70,22 @@ public enum Utils { return passphrase } } + + public static func showNotificationWithOTP(password: Password) { + guard let otp = password.currentOtp else { + return + } + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.getNotificationSettings { state in + guard state.authorizationStatus == .authorized else { + return + } + let content = UNMutableNotificationContent() + content.title = "OTPFor".localize(password.name) + content.body = otp + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + let request = UNNotificationRequest(identifier: "otpNotification", content: content, trigger: trigger) + notificationCenter.add(request) + } + } }