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:
parent
5057528ad9
commit
763cddf540
9 changed files with 67 additions and 69 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue