Show notification with OTP after providing password through extension (#509)

* Allow to do something with a password after providing it in the extension
* Make fields non-nil
* Show OTP in notification after providing a password through extension
This commit is contained in:
Danny Mösch 2021-09-20 09:50:05 +02:00 committed by GitHub
parent 5057528ad9
commit 763cddf540
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 67 additions and 69 deletions

View file

@ -9,6 +9,7 @@
import passKit import passKit
import SVProgressHUD import SVProgressHUD
import UIKit import UIKit
import UserNotifications
extension UIStoryboard { extension UIStoryboard {
static var passwordNavigationViewController: PasswordNavigationViewController { static var passwordNavigationViewController: PasswordNavigationViewController {
@ -75,6 +76,12 @@ class PasswordNavigationViewController: UIViewController {
configureTableView(in: parentPasswordEntity) configureTableView(in: parentPasswordEntity)
configureNotification() configureNotification()
configureSearchBar() configureSearchBar()
requestNotificationPermission()
}
private func requestNotificationPermission() {
let permissionOptions = UNAuthorizationOptions(arrayLiteral: .alert)
UNUserNotificationCenter.current().requestAuthorization(options: permissionOptions) { _, _ in }
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {

View file

@ -11,16 +11,13 @@ import SVProgressHUD
import UIKit import UIKit
class PasswordManager { class PasswordManager {
weak var viewController: UIViewController? private let viewController: UIViewController
init(viewController: UIViewController) { init(viewController: UIViewController) {
self.viewController = viewController self.viewController = viewController
} }
func providePasswordPasteboard(with passwordPath: String) { func providePasswordPasteboard(with passwordPath: String) {
guard let viewController = viewController else {
return
}
decryptPassword(in: viewController, with: passwordPath) { password in decryptPassword(in: viewController, with: passwordPath) { password in
SecurePasteboard.shared.copy(textToCopy: password.password) SecurePasteboard.shared.copy(textToCopy: password.password)
SVProgressHUD.setDefaultMaskType(.black) SVProgressHUD.setDefaultMaskType(.black)
@ -31,10 +28,6 @@ class PasswordManager {
} }
func addPassword(with password: Password) { func addPassword(with password: Password) {
guard let viewController = viewController else {
return
}
encryptPassword(in: viewController, with: password) { encryptPassword(in: viewController, with: password) {
SVProgressHUD.setDefaultMaskType(.black) SVProgressHUD.setDefaultMaskType(.black)
SVProgressHUD.setDefaultStyle(.light) SVProgressHUD.setDefaultStyle(.light)

View file

@ -22,6 +22,7 @@
"HmacBased" = "HMAC-basiert"; "HmacBased" = "HMAC-basiert";
"None" = "Kein valides Token"; "None" = "Kein valides Token";
"ExpiresIn" = "(läuft in %ds ab)"; "ExpiresIn" = "(läuft in %ds ab)";
"OTPFor" = "Einmalpasswort für %@";
// General (error) messages // General (error) messages
"Error" = "Fehler"; "Error" = "Fehler";

View file

@ -22,6 +22,7 @@
"HmacBased" = "HMAC-based"; "HmacBased" = "HMAC-based";
"None" = "None"; "None" = "None";
"ExpiresIn" = "(expires in %ds)"; "ExpiresIn" = "(expires in %ds)";
"OTPFor" = "One-time Password for %@";
// General (error) messages // General (error) messages
"Error" = "Error"; "Error" = "Error";

View file

@ -19,7 +19,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
}() }()
private lazy var credentialProvider: CredentialProvider = { [unowned self] in 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) private lazy var passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false)

View file

@ -10,13 +10,16 @@ import AuthenticationServices
import passKit import passKit
class CredentialProvider { class CredentialProvider {
var identifier: ASCredentialServiceIdentifier? private let viewController: UIViewController
weak var extensionContext: ASCredentialProviderExtensionContext? private let extensionContext: ASCredentialProviderExtensionContext
weak var viewController: UIViewController? 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.viewController = viewController
self.extensionContext = extensionContext self.extensionContext = extensionContext
self.afterDecryption = afterDecryption
} }
func credentials(for identity: ASPasswordCredentialIdentity) { func credentials(for identity: ASPasswordCredentialIdentity) {
@ -24,50 +27,35 @@ class CredentialProvider {
return return
} }
provideCredentials(in: viewController, with: recordIdentifier) { credential in decryptPassword(in: viewController, with: recordIdentifier) { password in
guard let credential = credential else { self.extensionContext.completeRequest(withSelectedCredential: .from(password))
return self.afterDecryption(password)
}
self.extensionContext?.completeRequest(withSelectedCredential: credential)
} }
} }
func persistAndProvideCredentials(with passwordPath: String) { func persistAndProvideCredentials(with passwordPath: String) {
provideCredentials(in: viewController, with: passwordPath) { credential in decryptPassword(in: viewController, with: passwordPath) { password in
guard let credential = credential else { if let identifier = self.identifier {
return ASCredentialIdentityStore.shared.getState { state in
} guard state.isEnabled else {
guard let credentialIdentity = provideCredentialIdentity(for: self.identifier, user: credential.user, recordIdentifier: passwordPath) else { return
self.extensionContext?.completeRequest(withSelectedCredential: credential) }
return let credentialIdentity = ASPasswordCredentialIdentity(
} serviceIdentifier: identifier,
user: password.getUsernameForCompletion(),
let store = ASCredentialIdentityStore.shared recordIdentifier: passwordPath
store.getState { state in )
if state.isEnabled {
ASCredentialIdentityStore.shared.saveCredentialIdentities([credentialIdentity]) 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? { extension ASPasswordCredential {
guard let serviceIdentifier = identifier else { static func from(_ password: Password) -> ASPasswordCredential {
return nil ASPasswordCredential(user: password.getUsernameForCompletion(), password: password.password)
}
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)
} }
} }

View file

@ -20,7 +20,7 @@ class ExtensionViewController: UIViewController {
}() }()
private lazy var credentialProvider: CredentialProvider = { [unowned self] in 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) private lazy var passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false)

View file

@ -11,22 +11,17 @@ import passKit
import UIKit import UIKit
class CredentialProvider { class CredentialProvider {
weak var extensionContext: NSExtensionContext? private let viewController: UIViewController
weak var 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.viewController = viewController
self.extensionContext = extensionContext self.extensionContext = extensionContext
self.afterDecryption = afterDecryption
} }
func provideCredentialsFindLogin(with passwordPath: String) { func provideCredentialsFindLogin(with passwordPath: String) {
guard let viewController = viewController else {
return
}
guard let extensionContext = extensionContext else {
return
}
decryptPassword(in: viewController, with: passwordPath) { password in decryptPassword(in: viewController, with: passwordPath) { password in
let extensionItem = NSExtensionItem() let extensionItem = NSExtensionItem()
var returnDictionary = [ var returnDictionary = [
@ -37,30 +32,25 @@ class CredentialProvider {
returnDictionary[PassExtensionKey.totpKey] = totpPassword returnDictionary[PassExtensionKey.totpKey] = totpPassword
} }
extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))] 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) { func provideCredentialsBrowser(with passwordPath: String) {
guard let viewController = viewController else {
return
}
guard let extensionContext = extensionContext else {
return
}
decryptPassword(in: viewController, with: passwordPath) { password in decryptPassword(in: viewController, with: passwordPath) { password in
Utils.copyToPasteboard(textToCopy: password.password) Utils.copyToPasteboard(textToCopy: password.password)
// return a dictionary for JavaScript for best-effor fill in // return a dictionary for JavaScript for best-effor fill in
let extensionItem = NSExtensionItem() let extensionItem = NSExtensionItem()
let returnDictionary = [ let returnDictionary = [
NSExtensionJavaScriptFinalizeArgumentKey: [ NSExtensionJavaScriptFinalizeArgumentKey: [
"username": password.getUsernameForCompletion(), PassExtensionKey.usernameKey: password.getUsernameForCompletion(),
"password": password.password, PassExtensionKey.passwordKey: password.password,
], ],
] ]
extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))] extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))]
extensionContext.completeRequest(returningItems: [extensionItem]) self.extensionContext.completeRequest(returningItems: [extensionItem])
self.afterDecryption(password)
} }
} }
} }

View file

@ -70,4 +70,22 @@ public enum Utils {
return passphrase 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)
}
}
} }