Add notification action to copy OTP or just inform about the copied OTP (#513)

* Add notification action to copy OTP or just inform about the copied OTP

The notification either shows the current OTP which can be copied by a notification action or it shows just a hint to inform about the copied OTP. This depends on the new option "autoCopyOTP".

* Extract method

* Set type and style one-time
This commit is contained in:
Danny Mösch 2021-10-01 19:32:14 +02:00 committed by GitHub
parent 63e7235978
commit e1cbcb5d7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 155 additions and 76 deletions

View file

@ -78,6 +78,7 @@
30B00F5526D59562004DAC61 /* PasscodeLockViewControllerForExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B00F5426D59562004DAC61 /* PasscodeLockViewControllerForExtension.swift */; };
30B00F5626D597A8004DAC61 /* PasscodeLockViewControllerForExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B00F5426D59562004DAC61 /* PasscodeLockViewControllerForExtension.swift */; };
30B04860209A5141001013CA /* PasswordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B0485F209A5141001013CA /* PasswordTest.swift */; };
30B331772704DBEE00D64A99 /* NotificationCenterDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B331762704DBEE00D64A99 /* NotificationCenterDispatcher.swift */; };
30B4C7BA24084AAA008B86F7 /* PasswordGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B4C7B924084AAA008B86F7 /* PasswordGenerator.swift */; };
30BAC8C622E3BAAF00438475 /* TestBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAC8C422E3BAAF00438475 /* TestBase.swift */; };
30BAC8C722E3BAAF00438475 /* TestPGPKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAC8C522E3BAAF00438475 /* TestPGPKeys.swift */; };
@ -372,6 +373,7 @@
30A86F94230F237000F821A4 /* CryptoFrameworkTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFrameworkTest.swift; sourceTree = "<group>"; };
30B00F5426D59562004DAC61 /* PasscodeLockViewControllerForExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeLockViewControllerForExtension.swift; sourceTree = "<group>"; };
30B0485F209A5141001013CA /* PasswordTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordTest.swift; sourceTree = "<group>"; };
30B331762704DBEE00D64A99 /* NotificationCenterDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterDispatcher.swift; sourceTree = "<group>"; };
30B4C7B924084AAA008B86F7 /* PasswordGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordGenerator.swift; sourceTree = "<group>"; };
30BAC8C422E3BAAF00438475 /* TestBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestBase.swift; sourceTree = "<group>"; };
30BAC8C522E3BAAF00438475 /* TestPGPKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestPGPKeys.swift; sourceTree = "<group>"; };
@ -742,10 +744,10 @@
9AFC87E025B3B556008D6060 /* Services */ = {
isa = PBXGroup;
children = (
9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */,
9AFC87EF25B514AD008D6060 /* PasswordDecryptor.swift */,
9AFC87FF25B51EC3008D6060 /* PasswordEncryptor.swift */,
9AFC87F725B51742008D6060 /* PasswordManager.swift */,
9AFC87E125B3B5C6008D6060 /* PasswordNavigationDataSource.swift */,
);
path = Services;
sourceTree = "<group>";
@ -875,6 +877,7 @@
30697C2421F63C590064FCAC /* Globals.swift */,
3032327322C7F710009EBD9C /* KeyFileManager.swift */,
30BAC8CC22E3BB9700438475 /* KeyStore.swift */,
30B331762704DBEE00D64A99 /* NotificationCenterDispatcher.swift */,
30697C2321F63C580064FCAC /* NotificationNames.swift */,
302202EE222F14E400555236 /* SearchBarScope.swift */,
30697C2721F63C590064FCAC /* Utils.swift */,
@ -1515,6 +1518,7 @@
A2AA934422DE30DD00D79A00 /* PGPAgent.swift in Sources */,
30697C3B21F63C990064FCAC /* String+Localization.swift in Sources */,
302E85612125ECC70031BA64 /* Parser.swift in Sources */,
30B331772704DBEE00D64A99 /* NotificationCenterDispatcher.swift in Sources */,
30CCA91A232591320048CA51 /* ObjectivePGPInterface.swift in Sources */,
30697C4621F63CAB0064FCAC /* GitCredential.swift in Sources */,
30B4C7BA24084AAA008B86F7 /* PasswordGenerator.swift in Sources */,

View file

@ -28,6 +28,14 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
return uiSwitch
}()
let autoCopyOTPSwitch: UISwitch = {
let uiSwitch = UISwitch()
uiSwitch.onTintColor = Colors.systemBlue
uiSwitch.sizeToFit()
uiSwitch.addTarget(self, action: #selector(autoCopyOTPSwitchAction(_:)), for: UIControl.Event.valueChanged)
return uiSwitch
}()
let rememberPGPPassphraseSwitch: UISwitch = {
let uiSwitch = UISwitch()
uiSwitch.onTintColor = Colors.systemBlue
@ -91,6 +99,7 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
[.title: "HidePasswordImages".localize(), .action: "none"],
[.title: "HideUnknownFields".localize(), .action: "none"],
[.title: "HideOtpFields".localize(), .action: "none"],
[.title: "AutoCopyOTP".localize(), .action: "none"],
],
]
super.viewDidLoad()
@ -98,71 +107,64 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
cell.accessoryType = .none
cell.selectionStyle = .none
switch cell.textLabel!.text! {
case "HideUnknownFields".localize():
cell.accessoryType = .none
let detailButton = UIButton(type: .detailDisclosure)
hideUnknownSwitch.frame = CGRect(x: detailButton.bounds.width + 10, y: 0, width: hideUnknownSwitch.bounds.width, height: hideUnknownSwitch.bounds.height)
detailButton.frame = CGRect(x: 0, y: 5, width: detailButton.bounds.width, height: detailButton.bounds.height)
detailButton.addTarget(self, action: #selector(GeneralSettingsTableViewController.tapHideUnknownSwitchDetailButton(_:)), for: UIControl.Event.touchDown)
let accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: detailButton.bounds.width + hideUnknownSwitch.bounds.width + 10, height: cell.contentView.bounds.height))
accessoryView.addSubview(detailButton)
accessoryView.addSubview(hideUnknownSwitch)
hideUnknownSwitch.center.y = accessoryView.center.y
detailButton.center.y = accessoryView.center.y
cell.accessoryView = accessoryView
cell.selectionStyle = .none
hideUnknownSwitch.isOn = Defaults.isHideUnknownOn
addDetailButton(to: cell, for: hideUnknownSwitch, with: #selector(tapHideUnknownSwitchDetailButton))
case "HideOtpFields".localize():
cell.accessoryType = .none
let detailButton = UIButton(type: .detailDisclosure)
hideOTPSwitch.frame = CGRect(x: detailButton.bounds.width + 10, y: 0, width: hideOTPSwitch.bounds.width, height: hideOTPSwitch.bounds.height)
detailButton.frame = CGRect(x: 0, y: 5, width: detailButton.bounds.width, height: detailButton.bounds.height)
detailButton.addTarget(self, action: #selector(GeneralSettingsTableViewController.tapHideOTPSwitchDetailButton(_:)), for: UIControl.Event.touchDown)
let accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: detailButton.bounds.width + hideOTPSwitch.bounds.width + 10, height: cell.contentView.bounds.height))
accessoryView.addSubview(detailButton)
accessoryView.addSubview(hideOTPSwitch)
hideOTPSwitch.center.y = accessoryView.center.y
detailButton.center.y = accessoryView.center.y
cell.accessoryView = accessoryView
cell.selectionStyle = .none
hideOTPSwitch.isOn = Defaults.isHideOTPOn
addDetailButton(to: cell, for: hideOTPSwitch, with: #selector(tapHideOTPSwitchDetailButton))
case "RememberPgpKeyPassphrase".localize():
cell.accessoryType = .none
cell.selectionStyle = .none
cell.accessoryView = rememberPGPPassphraseSwitch
case "RememberGitCredentialPassphrase".localize():
cell.accessoryType = .none
cell.selectionStyle = .none
cell.accessoryView = rememberGitCredentialPassphraseSwitch
case "ShowFolders".localize():
cell.accessoryType = .none
cell.selectionStyle = .none
cell.accessoryView = showFolderSwitch
case "EnableGPGID".localize():
cell.accessoryType = .none
cell.selectionStyle = .none
cell.accessoryView = enableGPGIDSwitch
case "HidePasswordImages".localize():
cell.accessoryType = .none
let detailButton = UIButton(type: .detailDisclosure)
hidePasswordImagesSwitch.frame = CGRect(x: detailButton.bounds.width + 10, y: 0, width: hidePasswordImagesSwitch.bounds.width, height: hidePasswordImagesSwitch.bounds.height)
detailButton.frame = CGRect(x: 0, y: 5, width: detailButton.bounds.width, height: detailButton.bounds.height)
detailButton.addTarget(self, action: #selector(GeneralSettingsTableViewController.tapHidePasswordImagesSwitchDetailButton(_:)), for: UIControl.Event.touchDown)
let accessoryView = UIView(frame: CGRect(x: 0, y: 0, width: detailButton.bounds.width + hidePasswordImagesSwitch.bounds.width + 10, height: cell.contentView.bounds.height))
accessoryView.addSubview(detailButton)
accessoryView.addSubview(hidePasswordImagesSwitch)
hidePasswordImagesSwitch.center.y = accessoryView.center.y
detailButton.center.y = accessoryView.center.y
cell.accessoryView = accessoryView
cell.selectionStyle = .none
hidePasswordImagesSwitch.isOn = Defaults.isHidePasswordImagesOn
addDetailButton(to: cell, for: hidePasswordImagesSwitch, with: #selector(tapHidePasswordImagesSwitchDetailButton))
case "AutoCopyOTP".localize():
autoCopyOTPSwitch.isOn = Defaults.autoCopyOTP
addDetailButton(to: cell, for: autoCopyOTPSwitch, with: #selector(tapAutoCopyOTPSwitchDetailButton))
default:
break
}
return cell
}
private func addDetailButton(to cell: UITableViewCell, for uiSwitch: UISwitch, with action: Selector) {
let detailButton = UIButton(type: .detailDisclosure)
uiSwitch.frame = CGRect(
x: detailButton.bounds.width + 10,
y: 0,
width: uiSwitch.bounds.width,
height: uiSwitch.bounds.height
)
detailButton.frame = CGRect(
x: 0,
y: 5,
width: detailButton.bounds.width,
height: detailButton.bounds.height
)
detailButton.addTarget(self, action: action, for: UIControl.Event.touchDown)
let accessoryViewFrame = CGRect(
x: 0,
y: 0,
width: detailButton.bounds.width + uiSwitch.bounds.width + 10,
height: cell.contentView.bounds.height
)
let accessoryView = UIView(frame: accessoryViewFrame)
accessoryView.addSubview(detailButton)
accessoryView.addSubview(uiSwitch)
uiSwitch.center.y = accessoryView.center.y
detailButton.center.y = accessoryView.center.y
cell.accessoryView = accessoryView
}
@objc
func tapHideUnknownSwitchDetailButton(_: Any?) {
let alertMessage = "HideUnknownFieldsExplanation.".localize()
@ -178,6 +180,13 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
}
@objc
func tapAutoCopyOTPSwitchDetailButton(_: Any?) {
let alertMessage = "AutoCopyOTPExplanation.".localize()
let alertTitle = "AutoCopyOTP".localize()
Utils.alert(title: alertTitle, message: alertMessage, controller: self, completion: nil)
}
@objc
func tapHidePasswordImagesSwitchDetailButton(_: Any?) {
let alertMessage = "HidePasswordImagesExplanation.".localize()
@ -197,6 +206,11 @@ class GeneralSettingsTableViewController: BasicStaticTableViewController {
NotificationCenter.default.post(name: .passwordDetailDisplaySettingChanged, object: nil)
}
@objc
func autoCopyOTPSwitchAction(_: Any?) {
Defaults.autoCopyOTP = autoCopyOTPSwitch.isOn
}
@objc
func rememberPGPPassphraseSwitchAction(_: Any?) {
Defaults.isRememberPGPPassphraseOn = rememberPGPPassphraseSwitch.isOn

View file

@ -80,8 +80,26 @@ class PasswordNavigationViewController: UIViewController {
}
private func requestNotificationPermission() {
// Ask for permission to receive notifications
let notificationCenter = UNUserNotificationCenter.current()
let permissionOptions = UNAuthorizationOptions(arrayLiteral: .alert)
UNUserNotificationCenter.current().requestAuthorization(options: permissionOptions) { _, _ in }
notificationCenter.requestAuthorization(options: permissionOptions) { _, _ in }
// Register notification action
let copyAction = UNNotificationAction(
identifier: Globals.otpNotificationCopyAction,
title: "CopyToPasteboard".localize(),
options: UNNotificationActionOptions(rawValue: 0)
)
let otpCategory = UNNotificationCategory(
identifier: Globals.otpNotificationCategory,
actions: [copyAction],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: "",
options: []
)
notificationCenter.setNotificationCategories([otpCategory])
notificationCenter.delegate = NotificationCenterDispatcher.shared
}
override func viewWillAppear(_ animated: Bool) {

View file

@ -22,7 +22,11 @@
"HmacBased" = "HMAC-basiert";
"None" = "Kein valides Token";
"ExpiresIn" = "(läuft in %ds ab)";
"OTPFor" = "Einmalpasswort für %@";
"OTPForPassword" = "Einmalpasswort für %@";
"OTPForPasswordCopied" = "Einmalpasswort für %@ kopiert";
"CopyToPasteboard" = "In Zwischenablage kopieren";
"AutoCopyOTP" = "OTPs automatisch kopieren";
"AutoCopyOTPExplanation." = "Nachdem Login und Passwort automatisch in die vorgesehenen Felder gefüllt wurden, wird eine Benachrichtigung mit dem aktuellen Einmalpasswort angezeigt. Dieses kann in die Zwischenablage kopiert werden. Ist diese Option aktiviert, geschieht das Kopieren automatisch.";
// General (error) messages
"Error" = "Fehler";

View file

@ -22,7 +22,11 @@
"HmacBased" = "HMAC-based";
"None" = "None";
"ExpiresIn" = "(expires in %ds)";
"OTPFor" = "One-time Password for %@";
"OTPForPassword" = "One-time password for %@";
"OTPForPasswordCopied" = "One-time password for %@ copied";
"CopyToPasteboard" = "Copy to pasteboard";
"AutoCopyOTP" = "Automatically Copy OTPs";
"AutoCopyOTPExplanation." = "After username and password have been auto-filled into a form by Pass, a notification is shown with the current one-time password which can be copied to the pasteboard. Enabling this option automatically copies the current one-time password.";
// General (error) messages
"Error" = "Error";

View file

@ -19,7 +19,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
}()
private lazy var credentialProvider: CredentialProvider = { [unowned self] in
CredentialProvider(viewController: self, extensionContext: extensionContext, afterDecryption: Utils.showNotificationWithOTP)
CredentialProvider(viewController: self, extensionContext: extensionContext, afterDecryption: NotificationCenterDispatcher.showOTPNotification)
}()
private lazy var passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false)

View file

@ -20,7 +20,7 @@ class ExtensionViewController: UIViewController {
}()
private lazy var credentialProvider: CredentialProvider = { [unowned self] in
CredentialProvider(viewController: self, extensionContext: extensionContext!, afterDecryption: Utils.showNotificationWithOTP)
CredentialProvider(viewController: self, extensionContext: extensionContext!, afterDecryption: NotificationCenterDispatcher.showOTPNotification)
}()
private lazy var passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false)

View file

@ -57,4 +57,6 @@ public extension DefaultsKeys {
var passwordGenerator: DefaultsKey<PasswordGenerator> { .init("passwordGenerator", defaultValue: PasswordGenerator()) }
var encryptInArmored: DefaultsKey<Bool> { .init("encryptInArmored", defaultValue: false) }
var autoCopyOTP: DefaultsKey<Bool> { .init("autoCopyOTP", defaultValue: false) }
}

View file

@ -47,6 +47,10 @@ public final class Globals {
public static let oneTimePasswordDots = "••••••"
public static let passwordFont = UIFont(name: "Courier-Bold", size: UIFont.labelFontSize - 1)
public static let otpNotification = bundleIdentifier + ".notification.otp"
public static let otpNotificationCategory = bundleIdentifier + ".notification.otp.category"
public static let otpNotificationCopyAction = bundleIdentifier + ".notification.otp.action.copy"
// UI related
public static let tableCellButtonSize = CGFloat(20.0)

View file

@ -0,0 +1,47 @@
//
// NotificationCenterDispatcher.swift
// passKit
//
// Created by Danny Moesch on 29.09.21.
// Copyright © 2021 Bob Sun. All rights reserved.
//
public class NotificationCenterDispatcher: NSObject, UNUserNotificationCenterDelegate {
public static let shared = NotificationCenterDispatcher()
public func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
if response.actionIdentifier == Globals.otpNotificationCopyAction {
if let otp = response.notification.request.content.userInfo["otp"] as? String {
UIPasteboard.general.string = otp
}
}
completionHandler()
}
public static func showOTPNotification(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()
if Defaults.autoCopyOTP {
content.title = "OTPForPasswordCopied".localize(password.name)
} else {
content.title = "OTPForPassword".localize(password.name)
content.body = otp
content.categoryIdentifier = Globals.otpNotificationCategory
content.userInfo = [
"path": password.namePath,
"otp": otp,
]
}
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: Globals.otpNotification, content: content, trigger: trigger)
notificationCenter.add(request)
}
}
}

View file

@ -70,22 +70,4 @@ 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)
}
}
}