Refactor and merge code of extensions

This commit is contained in:
Mingshen Sun 2021-01-10 13:40:17 -08:00
parent 87d1dd5be1
commit 776884e894
No known key found for this signature in database
GPG key ID: 1F86BA2052FED3B4
13 changed files with 391 additions and 446 deletions

View file

@ -1,85 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="QHc-XA-1MZ">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Xzf-qb-wq7">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Password Store-->
<scene sceneID="NlT-0d-7x9">
<objects>
<viewController id="DnC-Ka-AYb" customClass="ExtensionViewController" customModule="passExtension" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="kSO-tI-r1g"/>
<viewControllerLayoutGuide type="bottom" id="TYK-kT-vkH"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="g9r-Vt-nbj">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="KNT-Mp-tgV">
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<searchBar key="tableHeaderView" contentMode="redraw" translucent="NO" id="xuO-jY-YRU">
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
<autoresizingMask key="autoresizingMask" flexibleMaxY="YES"/>
<color key="barTintColor" systemColor="secondarySystemBackgroundColor"/>
<textInputTraits key="textInputTraits"/>
</searchBar>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="passwordTableViewCell" textLabel="LUo-8T-I4j" detailTextLabel="9ik-sy-sTS" style="IBUITableViewCellStyleValue1" id="T2b-vj-fza" customClass="PasswordTableViewCell" customModule="passExtension" customModuleProvider="target">
<rect key="frame" x="0.0" y="84" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="T2b-vj-fza" id="aVb-V4-hqg">
<rect key="frame" x="0.0" y="0.0" width="348" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="LUo-8T-I4j">
<rect key="frame" x="16" y="12" width="33" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="9ik-sy-sTS">
<rect key="frame" x="296" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
</tableView>
</subviews>
<constraints>
<constraint firstItem="KNT-Mp-tgV" firstAttribute="top" secondItem="kSO-tI-r1g" secondAttribute="bottom" id="1Mo-as-yNc"/>
<constraint firstItem="TYK-kT-vkH" firstAttribute="top" secondItem="KNT-Mp-tgV" secondAttribute="bottom" id="RUo-Zw-D5C"/>
<constraint firstItem="KNT-Mp-tgV" firstAttribute="leading" secondItem="g9r-Vt-nbj" secondAttribute="leading" id="Tre-Kp-RiI"/>
<constraint firstAttribute="trailing" secondItem="KNT-Mp-tgV" secondAttribute="trailing" id="YPI-WX-ffM"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Password Store" id="MEN-Kg-v16">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="CH4-D6-aFB">
<connections>
<action selector="cancelExtension:" destination="DnC-Ka-AYb" id="In1-WB-K8r"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="searchBar" destination="xuO-jY-YRU" id="5Gk-EN-nKb"/>
<outlet property="tableView" destination="KNT-Mp-tgV" id="XdF-42-lk8"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="RYa-GM-dIn" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1712.8" y="10.344827586206897"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="oy9-wd-tIc">
<objects>
@ -91,17 +20,51 @@
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="DnC-Ka-AYb" kind="relationship" relationship="rootViewController" id="Yes-tn-lzA"/>
<segue destination="IaK-yg-fFt" kind="relationship" relationship="rootViewController" id="3T0-Cv-Bfg"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cpm-jG-Meg" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="772" y="9.4452773613193415"/>
</scene>
<!--Extension View Controller-->
<scene sceneID="Xbh-hw-0pj">
<objects>
<viewController id="Xzf-qb-wq7" customClass="ExtensionViewController" customModule="passExtension" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JIb-8X-ysu">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="hzP-Y0-FLR">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<connections>
<segue destination="QHc-XA-1MZ" kind="embed" id="46u-rW-D7k"/>
</connections>
</containerView>
</subviews>
<viewLayoutGuide key="safeArea" id="6iy-0F-jx6"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vBS-EH-ZFM" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-28" y="9"/>
</scene>
<!--SearchPassword-->
<scene sceneID="bv5-bN-jAX">
<objects>
<viewControllerPlaceholder storyboardName="SearchPassword" id="IaK-yg-fFt" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="ZjU-eF-yp6"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="FKe-r2-5pi" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1590" y="9"/>
</scene>
</scenes>
<resources>
<systemColor name="secondarySystemBackgroundColor">
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View file

@ -10,220 +10,158 @@ import Foundation
import MobileCoreServices
import passKit
class ExtensionViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UINavigationBarDelegate {
@IBOutlet var searchBar: UISearchBar!
@IBOutlet var tableView: UITableView!
class ExtensionViewController: UIViewController {
var passcodelock: PasscodeExtensionDisplay {
PasscodeExtensionDisplay(extensionContext: self.extensionContext!)
}
private let passwordStore = PasswordStore.shared
private let keychain = AppKeychain.shared
var embeddedNavigationController: UINavigationController {
children.first as! UINavigationController
}
private var searchActive = false
private var passwordsTableEntries: [PasswordTableEntry] = []
private var filteredPasswordsTableEntries: [PasswordTableEntry] = []
var passwordsViewController: PasswordsViewController {
embeddedNavigationController.viewControllers.first as! PasswordsViewController
}
enum Action {
case findLogin, fillBrowser, unknown
}
private var extensionAction = Action.unknown
private var action = Action.unknown
private lazy var passcodelock: PasscodeExtensionDisplay = {
let passcodelock = PasscodeExtensionDisplay(extensionContext: self.extensionContext)
return passcodelock
}()
lazy var credentialProvider = CredentialProvider(viewController: self, extensionContext: self.extensionContext!)
private func initPasswordsTableEntries() {
filteredPasswordsTableEntries.removeAll()
override func viewDidLoad() {
super.viewDidLoad()
passcodelock.presentPasscodeLockIfNeeded(self)
let passwordEntities = passwordStore.fetchPasswordEntityCoreData(withDir: false)
passwordsTableEntries = passwordEntities.map {
PasswordTableEntry($0)
}
let passwordsTableEntries = PasswordStore.shared.fetchPasswordEntityCoreData(withDir: false).compactMap { PasswordTableEntry($0) }
let dataSource = PasswordsTableDataSource(entries: passwordsTableEntries)
passwordsViewController.dataSource = dataSource
passwordsViewController.selectionDelegate = self
passwordsViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel))
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
passcodelock.presentPasscodeLockIfNeeded(self)
prepareCredentialList()
}
override func viewDidLoad() {
super.viewDidLoad()
// prepare
searchBar.delegate = self
tableView.delegate = self
tableView.dataSource = self
tableView.register(PasswordTableViewCell.self, forCellReuseIdentifier: "passwordTableViewCell")
@objc
private func cancel(_: AnyObject?) {
self.extensionContext?.completeRequest(returningItems: nil)
}
// initialize table entries
initPasswordsTableEntries()
// get the provider
guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
func prepareCredentialList() {
guard let attachments = self.extensionContext?.attachments else {
return
}
for extensionItem in extensionItems {
guard let itemProviders = extensionItem.attachments else {
continue
}
for provider in itemProviders {
// search using the extensionContext inputs
if provider.hasItemConformingToTypeIdentifier(OnePasswordExtensionActions.findLogin) {
provider.loadItem(forTypeIdentifier: OnePasswordExtensionActions.findLogin, options: nil) { item, _ in
self.updateExtension(with: self.getUrl(from: item as! NSDictionary), action: .findLogin)
}
} else if provider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) {
provider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil) { item, _ in
if let dictionary = item as? NSDictionary, let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary {
self.updateExtension(with: self.getUrl(from: results))
}
}
} else if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { item, _ in
self.updateExtension(with: (item as? NSURL)!.host)
}
}
func completeTask(_ text: String?) {
DispatchQueue.main.async {
self.passwordsViewController.showPasswordsWithSuggstion(matching: text ?? "")
self.passwordsViewController.navigationItem.prompt = text
}
}
}
private func getUrl(from dictionary: NSDictionary) -> String? {
if var urlString = dictionary[OnePasswordExtensionKey.URLStringKey] as? String {
if !urlString.hasPrefix("http://"), !urlString.hasPrefix("https://") {
urlString = "http://" + urlString
}
return URL(string: urlString)?.host
}
return nil
}
private func updateExtension(with url: String?, action: Action = .fillBrowser) {
// Set text, set active, and force search.
DispatchQueue.main.async { [weak self] in
self?.extensionAction = action
self?.searchBar.text = url
self?.searchBar.becomeFirstResponder()
self?.searchBarSearchButtonClicked((self?.searchBar)!)
}
}
// define cell contents, and set long press action
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "passwordTableViewCell", for: indexPath) as! PasswordTableViewCell
let entry = getPasswordEntry(by: indexPath)
cell.configure(with: entry)
return cell
}
// select row -> extension returns (with username and password)
func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
let entry = getPasswordEntry(by: indexPath)
guard PGPAgent.shared.isPrepared else {
Utils.alert(title: "CannotCopyPassword".localize(), message: "PgpKeyNotSet.".localize(), controller: self, completion: nil)
return
}
let passwordEntity = entry.passwordEntity
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
decryptPassword(passwordEntity: passwordEntity)
}
private func decryptPassword(passwordEntity: PasswordEntity, keyID: String? = nil) {
DispatchQueue.global(qos: .userInteractive).async {
do {
let requestPGPKeyPassphrase = Utils.createRequestPGPKeyPassphraseHandler(controller: self)
let decryptedPassword = try self.passwordStore.decrypt(passwordEntity: passwordEntity, keyID: keyID, requestPGPKeyPassphrase: requestPGPKeyPassphrase)
let username = decryptedPassword.getUsernameForCompletion()
let password = decryptedPassword.password
DispatchQueue.main.async {
// prepare a dictionary to return
switch self.extensionAction {
case .findLogin:
let extensionItem = NSExtensionItem()
var returnDictionary = [
OnePasswordExtensionKey.usernameKey: username,
OnePasswordExtensionKey.passwordKey: password,
]
if let totpPassword = decryptedPassword.currentOtp {
returnDictionary[OnePasswordExtensionKey.totpKey] = totpPassword
}
extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))]
self.extensionContext!.completeRequest(returningItems: [extensionItem], completionHandler: nil)
case .fillBrowser:
Utils.copyToPasteboard(textToCopy: decryptedPassword.password)
// return a dictionary for JavaScript for best-effor fill in
let extensionItem = NSExtensionItem()
let returnDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: ["username": username, "password": password]]
extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))]
self.extensionContext?.completeRequest(returningItems: [extensionItem], completionHandler: nil)
default:
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
}
} catch let AppError.pgpPrivateKeyNotFound(keyID: key) {
DispatchQueue.main.async {
// alert: cancel or try again
let alert = UIAlertController(title: "CannotShowPassword".localize(), message: AppError.pgpPrivateKeyNotFound(keyID: key).localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction.cancelAndPopView(controller: self))
let selectKey = UIAlertAction.selectKey(controller: self) { action in
self.decryptPassword(passwordEntity: passwordEntity, keyID: action.title)
}
alert.addAction(selectKey)
self.present(alert, animated: true, completion: nil)
}
} catch {
DispatchQueue.main.async {
Utils.alert(title: "CannotCopyPassword".localize(), message: error.localizedDescription, controller: self, completion: nil)
DispatchQueue.global(qos: .userInitiated).async {
for attachment in attachments {
if attachment.hasURL {
self.action = .fillBrowser
attachment.extractSearchText { completeTask($0) }
} else if attachment.hasFindLoginAction {
self.action = .findLogin
attachment.extractSearchText { completeTask($0) }
} else if attachment.hasPropertyList {
self.action = .fillBrowser
attachment.extractSearchText { completeTask($0) }
} else {
self.action = .unknown
}
}
}
}
func numberOfSectionsInTableView(tableView _: UITableView) -> Int {
1
}
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
if searchActive {
return filteredPasswordsTableEntries.count
}
return passwordsTableEntries.count
}
@IBAction
private func cancelExtension(_: Any) {
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.text = ""
searchActive = false
tableView.reloadData()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if let searchText = searchBar.text, searchText.isEmpty == false {
filteredPasswordsTableEntries = passwordsTableEntries.filter { $0.match(searchText) }
searchActive = true
} else {
searchActive = false
}
tableView.reloadData()
}
func searchBar(_ searchBar: UISearchBar, textDidChange _: String) {
searchBarSearchButtonClicked(searchBar)
}
private func getPasswordEntry(by indexPath: IndexPath) -> PasswordTableEntry {
if searchActive {
return filteredPasswordsTableEntries[indexPath.row]
} else {
return passwordsTableEntries[indexPath.row]
}
}
}
extension ExtensionViewController: PasswordSelectionDelegate {
func selected(password: PasswordTableEntry) {
switch action {
case .findLogin:
credentialProvider.provideCredentialsFindLogin(with: password.passwordEntity.getPath())
case .fillBrowser:
credentialProvider.provideCredentialsBrowser(with: password.passwordEntity.getPath())
default:
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
}
}
extension NSDictionary {
func extractSearchText() -> String? {
if let value = self[PassExtensionKey.URLStringKey] as? String {
if let host = URL(string: value)?.host {
return host
} else {
return value
}
} else if let value = self[NSExtensionJavaScriptPreprocessingResultsKey] as? String {
if let host = URL(string: value)?.host {
return host
} else {
return value
}
}
return nil
}
}
extension NSItemProvider {
var hasFindLoginAction: Bool {
hasItemConformingToTypeIdentifier(PassExtensionActions.findLogin)
}
var hasURL: Bool {
hasItemConformingToTypeIdentifier(kUTTypeURL as String) && registeredTypeIdentifiers.count == 1
}
var hasPropertyList: Bool {
hasItemConformingToTypeIdentifier(kUTTypePropertyList as String)
}
}
extension NSExtensionContext {
/// Get all the attachments to this post.
var attachments: [NSItemProvider] {
guard let items = inputItems as? [NSExtensionItem] else {
return []
}
return items.flatMap { $0.attachments ?? [] }
}
}
extension NSItemProvider {
/// Extracts the URL from the item provider
func extractSearchText(completion: @escaping (String?) -> Void) {
self.loadItem(forTypeIdentifier: kUTTypeURL as String) { item, _ in
if let url = item as? NSURL {
completion(url.host)
} else {
completion(nil)
}
}
self.loadItem(forTypeIdentifier: kUTTypePropertyList as String) { item, _ in
if let dict = item as? NSDictionary {
if let result = dict[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary {
completion(result.extractSearchText())
}
}
}
self.loadItem(forTypeIdentifier: PassExtensionActions.findLogin) { item, _ in
if let dict = item as? NSDictionary {
let text = dict.extractSearchText()
completion(text)
}
}
}
}

View file

@ -1,59 +0,0 @@
//
// PasscodeLockDisplay.swift
// pass
//
// Created by Yishi Lin on 14/6/17.
// Copyright © 2017 Bob Sun. All rights reserved.
//
import Foundation
import passKit
// cancel means cancel the extension
class PasscodeLockViewControllerForExtension: PasscodeLockViewController {
var originalExtensionContest: NSExtensionContext?
convenience init(extensionContext: NSExtensionContext?) {
self.init()
self.originalExtensionContest = extensionContext
}
override func viewDidLoad() {
super.viewDidLoad()
cancelButton?.removeTarget(nil, action: nil, for: .allEvents)
cancelButton?.addTarget(self, action: #selector(cancelExtension), for: .touchUpInside)
}
@objc
func cancelExtension() {
originalExtensionContest?.completeRequest(returningItems: [], completionHandler: nil)
}
}
class PasscodeExtensionDisplay {
private var isPasscodePresented = false
private let passcodeLockVC: PasscodeLockViewControllerForExtension
private let extensionContext: NSExtensionContext?
init(extensionContext: NSExtensionContext?) {
self.extensionContext = extensionContext
self.passcodeLockVC = PasscodeLockViewControllerForExtension(extensionContext: extensionContext)
passcodeLockVC.dismissCompletionCallback = { [weak self] in
self?.dismiss()
}
passcodeLockVC.setCancellable(true)
}
// present the passcode lock view if passcode is set and the view controller is not presented
func presentPasscodeLockIfNeeded(_ extensionVC: UIViewController) {
guard PasscodeLock.shared.hasPasscode, !isPasscodePresented == true else {
return
}
isPasscodePresented = true
extensionVC.present(passcodeLockVC, animated: true, completion: nil)
}
func dismiss(animated _: Bool = true) {
isPasscodePresented = false
}
}

View file

@ -7,7 +7,7 @@
//
// This file contains constants from https://github.com/agilebits/onepassword-app-extension/
enum OnePasswordExtensionActions {
enum PassExtensionActions {
static let findLogin = "org.appextension.find-login-action"
static let saveLogin = "org.appextension.save-login-action"
static let changePassword = "org.appextension.change-password-action"
@ -15,8 +15,8 @@ enum OnePasswordExtensionActions {
static let fillBrowser = "org.appextension.fill-browser-action"
}
enum OnePasswordExtensionKey {
// Login Dictionary keys - Used to get or set the properties of a 1Password Login
enum PassExtensionKey {
// Login Dictionary keys
static let URLStringKey = "url_string"
static let usernameKey = "username"
static let passwordKey = "password"
@ -29,23 +29,10 @@ enum OnePasswordExtensionKey {
static let oldPasswordKey = "old_password"
static let passwordGeneratorOptionsKey = "password_generator_options"
// Password Generator options - Used to set the 1Password Password Generator options when saving a new Login or when changing the password for for an existing Login
// Password Generator options
static let generatedPasswordMinLengthKey = "password_min_length"
static let generatedPasswordMaxLengthKey = "password_max_length"
static let generatedPasswordRequireDigitsKey = "password_require_digits"
static let generatedPasswordRequireSymbolsKey = "password_require_symbols"
static let generatedPasswordForbiddenCharactersKey = "password_forbidden_characters"
}
// Errors codes
enum OnePasswordExtensionError {
static let errorDomain = "OnePasswordExtension"
static let errorCodeCancelledByUser = 0
static let errorCodeAPINotAvailable = 1
static let errorCodeFailedToContactExtension = 2
static let errorCodeFailedToLoadItemProviderData = 3
static let errorCodeCollectFieldsScriptFailed = 4
static let errorCodeFillFieldsScriptFailed = 5
static let errorCodeUnexpectedData = 6
static let errorCodeFailedToObtainURLStringFromWebView = 7 // swiftlint:disable:this identifier_name
}

View file

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Pass</string>
<string>$(APP_DISPLAY_NAME)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -31,9 +31,9 @@
SUBQUERY (
$extensionItem.attachments,
$attachment,
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "org.appextension.find-login-action" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text"
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO &quot;org.appextension.find-login-action&quot; ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO &quot;public.url&quot; ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO &quot;public.plain-text&quot;
).@count == $extensionItem.attachments.@count
).@count == 1</string>
<key>NSExtensionJavaScriptPreprocessingFile</key>

View file

@ -0,0 +1,66 @@
//
// CredentialProvider.swift
// passExtension
//
// Created by Sun, Mingshen on 1/9/21.
// Copyright © 2021 Bob Sun. All rights reserved.
//
import UIKit
import MobileCoreServices
import passKit
class CredentialProvider {
weak var extensionContext: NSExtensionContext?
weak var viewController: UIViewController?
init(viewController: UIViewController, extensionContext: NSExtensionContext) {
self.viewController = viewController
self.extensionContext = extensionContext
}
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 = [
PassExtensionKey.usernameKey: password.getUsernameForCompletion(),
PassExtensionKey.passwordKey: password.password,
]
if let totpPassword = password.currentOtp {
returnDictionary[PassExtensionKey.totpKey] = totpPassword
}
extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))]
extensionContext.completeRequest(returningItems: [extensionItem])
}
}
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,
],
]
extensionItem.attachments = [NSItemProvider(item: returnDictionary as NSSecureCoding, typeIdentifier: String(kUTTypePropertyList))]
extensionContext.completeRequest(returningItems: [extensionItem])
}
}
}