diff --git a/AgileWorks/AgileWorks.xcodeproj/project.pbxproj b/AgileWorks/AgileWorks.xcodeproj/project.pbxproj index ce4ac23f96663f55012299cb0bd62ceec00d1465..db10737a4736ce30199b2bb00f9305b6a50ab9be 100644 --- a/AgileWorks/AgileWorks.xcodeproj/project.pbxproj +++ b/AgileWorks/AgileWorks.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 7549FC9227EAF42C00FF9E0C /* Chinese-Simplified.strings in Resources */ = {isa = PBXBuildFile; fileRef = 75917F9F27BDF2050051E201 /* Chinese-Simplified.strings */; }; 7549FC9327EAF42C00FF9E0C /* Chinese-Traditional.strings in Resources */ = {isa = PBXBuildFile; fileRef = 75917FA127BDF2250051E201 /* Chinese-Traditional.strings */; }; 756C7E7B282B6A5700C24F4D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 756C7E7A282B6A5700C24F4D /* Assets.xcassets */; }; + 758F1BC02A5408D400DDEBC5 /* ClientCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758F1BBF2A5408D400DDEBC5 /* ClientCertificateViewController.swift */; }; 75917F9C27BDF0F10051E201 /* Japanese.strings in Resources */ = {isa = PBXBuildFile; fileRef = 75917F9B27BDF0F10051E201 /* Japanese.strings */; }; 75917F9E27BDF1C30051E201 /* English.strings in Resources */ = {isa = PBXBuildFile; fileRef = 75917F9D27BDF1C30051E201 /* English.strings */; }; 75917FA027BDF2050051E201 /* Chinese-Simplified.strings in Resources */ = {isa = PBXBuildFile; fileRef = 75917F9F27BDF2050051E201 /* Chinese-Simplified.strings */; }; @@ -279,6 +280,7 @@ 7576916A2A2EFEEC00EAFCBC /* AgileWorksRelease-Production.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "AgileWorksRelease-Production.entitlements"; sourceTree = ""; }; 7576916B2A2F003400EAFCBC /* TodayExtensionRelease-Production.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "TodayExtensionRelease-Production.entitlements"; sourceTree = ""; }; 757691732A32E5F100EAFCBC /* WidgetExtensionRelease-Production.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "WidgetExtensionRelease-Production.entitlements"; sourceTree = ""; }; + 758F1BBF2A5408D400DDEBC5 /* ClientCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCertificateViewController.swift; sourceTree = ""; }; 75917F9227BD315A0051E201 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 75917F9627BD317E0051E201 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 75917F9B27BDF0F10051E201 /* Japanese.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Japanese.strings; sourceTree = ""; }; @@ -680,6 +682,7 @@ children = ( VRZQMTKT1ZML0OSDGAKBK8ID /* LoginViewController.storyboard */, TXU9U01HBM9MEYSGQITWCSE9 /* LoginViewController.swift */, + 758F1BBF2A5408D400DDEBC5 /* ClientCertificateViewController.swift */, ); path = View; sourceTree = ""; @@ -1583,6 +1586,7 @@ UMOPI6WFDHHR8CWFUZDQE9U2 /* LoginUseCase.swift in Sources */, BD6C1AED24A480C70057756F /* DeviceService.swift in Sources */, 76AE530025D3626800AFA45A /* LicenseViewController.swift in Sources */, + 758F1BC02A5408D400DDEBC5 /* ClientCertificateViewController.swift in Sources */, BDAB1F6D240D0D0000EA15FD /* UserDefaultsExtensions.swift in Sources */, C50CF78E27D86DC90042C210 /* OpenLicenseBuilder.swift in Sources */, BD969631240C9CD400521925 /* SessionInfo.swift in Sources */, diff --git a/AgileWorks/AgileWorks/App/AppDelegate.swift b/AgileWorks/AgileWorks/App/AppDelegate.swift index 468a5ca56b3734eb1abb83fff5815b081972e376..90cefab25d84003d8081b233df8f2e40b21618fb 100644 --- a/AgileWorks/AgileWorks/App/AppDelegate.swift +++ b/AgileWorks/AgileWorks/App/AppDelegate.swift @@ -60,7 +60,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } private func setupAppAuth() { - OIDURLSessionProvider.setSession(Session.shared) + OIDURLSessionProvider.setSession(Session.shared(serverNumber: nil)) } private func getTapLink(url: URL) -> String? { diff --git a/AgileWorks/AgileWorks/Common/Service/DeviceService.swift b/AgileWorks/AgileWorks/Common/Service/DeviceService.swift index d2ee712a11215013ea82b4bb491274275a5c9fd0..fda742365c68d36fd8c9c30b11110aab48069e2f 100644 --- a/AgileWorks/AgileWorks/Common/Service/DeviceService.swift +++ b/AgileWorks/AgileWorks/Common/Service/DeviceService.swift @@ -69,7 +69,7 @@ public class DeviceService { let request = DeviceRequest(deleteFlag: true) let deleteDevice = DeleteDeviceEndpoint(request: request, serverNumber: serverNumber) - Session.send(deleteDevice) { result in + Session.send(serverNumber: serverNumber, deleteDevice) { result in let completionArg: APIResult switch result { case .success: diff --git a/AgileWorks/AgileWorks/Login/View/ClientCertificateViewController.swift b/AgileWorks/AgileWorks/Login/View/ClientCertificateViewController.swift new file mode 100644 index 0000000000000000000000000000000000000000..c80a6886ae5024418e33c4a12f262b704f51d431 --- /dev/null +++ b/AgileWorks/AgileWorks/Login/View/ClientCertificateViewController.swift @@ -0,0 +1,96 @@ +// +// ClientCertificateTableViewController.swift +// AgileWorks +// +// Created by Azuma Kasumi on 2023/07/04. +// Copyright © 2023 ATLED CORP. All rights reserved. +// + +import UIKit + +class ClientCertificateViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { + @IBOutlet weak var tableView: UITableView! + //インポート済みの証明書リスト + var certificates = CertificateDataStore().readAllClientCertificate() + override func viewDidLoad() { + super.viewDidLoad() + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + + tableView.dataSource = self + tableView.delegate = self + + tableView.allowsMultipleSelection = false + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + + cell.selectionStyle = UITableViewCell.SelectionStyle.none + + //ラベル名取得 + let dic = certificates![indexPath.row] + let label = dic[kSecAttrLabel as String] as! String + cell.textLabel?.text = label + + //選択中のラベルの場合はチェックマークをつける + let selectedLabel = KeychainDataStore().readCertificateLabel() + if selectedLabel == label { + cell.accessoryType = .checkmark + self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) + } + return cell + } + + //セルの数 + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if certificates == nil { + return 0 + } + return certificates!.count + } + + // セルが選択された時に呼び出される + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let cell = tableView.cellForRow(at: indexPath) + // チェックマークを入れる + cell!.accessoryType = .checkmark + + let dic = certificates![indexPath.row] + let label = dic[kSecAttrLabel as String] as! String + + let selectedLabel = KeychainDataStore().readCertificateLabel() + if selectedLabel != label { + //キーチェーンにラベル名を保存 + KeychainDataStore().writeCertificateLabel(label: label) + } else { + //設定済みの場合は解除 + cell!.accessoryType = .none + KeychainDataStore().removeCertificateLabel() + } + } + + // セルの選択が外れた時に呼び出される + func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + let cell = tableView.cellForRow(at: indexPath) + // チェックマークを外す + cell!.accessoryType = .none + } + + //クライアント証明書をスワイプ削除 + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + let dic = certificates![indexPath.row] + let label = dic[kSecAttrLabel as String] as! String + CertificateDataStore().removeClientCertificate(label: label) + + //選択中の証明書が削除された場合 + let selectedLabel = KeychainDataStore().readCertificateLabel() + if selectedLabel == label { + KeychainDataStore().removeCertificateLabel() + } + certificates = CertificateDataStore().readAllClientCertificate() + + tableView.deleteRows(at: [indexPath], with: .automatic) + } + } +} diff --git a/AgileWorks/AgileWorks/Login/View/LoginViewController.storyboard b/AgileWorks/AgileWorks/Login/View/LoginViewController.storyboard index ae4f5c3a6f0b89c7653e25dd2d476c912775e7df..422edd7862018de1a591d20e5922ff3c3567b777 100644 --- a/AgileWorks/AgileWorks/Login/View/LoginViewController.storyboard +++ b/AgileWorks/AgileWorks/Login/View/LoginViewController.storyboard @@ -1,9 +1,9 @@ - + - + @@ -19,17 +19,17 @@ - + - + - + - + @@ -95,21 +95,31 @@ - + @@ -208,7 +218,7 @@ - + @@ -227,11 +237,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - @@ -245,7 +286,7 @@ - + diff --git a/AgileWorks/AgileWorks/Login/View/LoginViewController.swift b/AgileWorks/AgileWorks/Login/View/LoginViewController.swift index 7ea78a97cae39c257487280f46cb1f6c7ccf9dac..e96cf0acf3d840cecbe96dba4e82fca62d7c4133 100644 --- a/AgileWorks/AgileWorks/Login/View/LoginViewController.swift +++ b/AgileWorks/AgileWorks/Login/View/LoginViewController.swift @@ -30,7 +30,7 @@ class LoginViewController: UIViewController { @IBOutlet private var disablingView: UIView! @IBOutlet private var clientCertificateLabel: UILabel! @IBOutlet private var clientCertificateSettingView: UIView! - @IBOutlet private var clientCertificateRemovalButton: UIButton! + @IBOutlet weak var clientCertificateSelectButton: UIButton! @IBOutlet private var qrCodeReadLabel: UILabel! @IBOutlet private var qrCodeReadView: UIView! @IBOutlet private var qrCodeStartupButton: UIButton! @@ -52,6 +52,8 @@ class LoginViewController: UIViewController { return context } + private var navVC: UINavigationController? = nil + let qrCodeReader = QRCodeReader() func inject(presenter: LoginPresenter) { @@ -85,7 +87,9 @@ class LoginViewController: UIViewController { } } - clientCertificateSettingView.isHidden = (CertificateDataStore().readClientCertificate() == nil) + let certificates = CertificateDataStore().readAllClientCertificate() + clientCertificateSettingView.isHidden = certificates?.isEmpty ?? true + //固定文言表示 setupFixedWording() @@ -174,6 +178,26 @@ class LoginViewController: UIViewController { self.view.endEditing(true) } + @IBAction func clientCertificateSelectButtonTapped(_ sender: Any) { + let csVC = clientCertificateSelectView() + navVC = UINavigationController(rootViewController: csVC) + //navVC.setNavigationBarHidden(false, animated: true) + let chevronLeftImage: UIImage? = UIImage(systemName: "chevron.left") + let backButton = UIBarButtonItem(image: chevronLeftImage, style: .plain, target: self, action: #selector(onTapBackButton(_:))) + navVC!.navigationBar.topItem!.leftBarButtonItem = backButton + navVC!.modalPresentationStyle = .fullScreen + self.present(navVC!, animated: true) + } + @objc func onTapBackButton(_ sender: UIBarButtonItem) { + navVC?.dismiss(animated: true) + } + + private func clientCertificateSelectView() -> UIViewController { + let storyboard = UIStoryboard(name: "LoginViewController", bundle: nil) + let vc = storyboard.instantiateViewController(withIdentifier: "ClientCertificateTableViewController") + return vc + } + @IBAction private func loginTapped() { guard let serverHost = serverHost else { return } guard let context = context else { return } @@ -339,7 +363,7 @@ class LoginViewController: UIViewController { private func setLoginControlesEnabled(_ enabled: Bool) { disablingView.isHidden = enabled - let controls: [UIControl] = [serverTextField, contextTextField, loginButton, clientCertificateRemovalButton, qrCodeStartupButton] + let controls: [UIControl] = [serverTextField, contextTextField, loginButton, clientCertificateSelectButton, qrCodeStartupButton] for view in controls { view.isEnabled = enabled } @@ -359,10 +383,11 @@ class LoginViewController: UIViewController { } } + @IBAction private func clientCertificateRemovalTapped(_ sender: Any) { let alertController = UIAlertController(title: "", message: getLocalizableStrings(key: "ClientCertificateRemovalConfirmation", comment: ""), preferredStyle: .alert) let okAction = UIAlertAction(title: getLocalizableStrings(key: "ClientCertificateRemovalOK", comment: ""), style: .default) { _ in - CertificateDataStore().removeClientCertificate() + CertificateDataStore().removeClientCertificate(label: "") self.clientCertificateSettingView.isHidden = true } let cancelAction = UIAlertAction(title: getLocalizableStrings(key: "ClientCertificateRemovalCancel", comment: ""), style: .cancel, handler: nil) @@ -370,7 +395,7 @@ class LoginViewController: UIViewController { alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) } - + private func devcie(serverNumber: Int, completion: @escaping (APIResult) -> Void) { DeviceService().deleteDevice(serverNumber: serverNumber) { result in switch result { diff --git a/AgileWorks/AgileWorks/Strings/Chinese-Simplified.strings b/AgileWorks/AgileWorks/Strings/Chinese-Simplified.strings index f522d3b855b1f13d10b6915ef5a66642b9529f91..a3ae55ff948272eb52216aaac83cbccbd8df8e46 100644 --- a/AgileWorks/AgileWorks/Strings/Chinese-Simplified.strings +++ b/AgileWorks/AgileWorks/Strings/Chinese-Simplified.strings @@ -104,6 +104,7 @@ "CertificateImportSuccess" = "输入的客户证书。"; "IncorrectPassphrase" = "通行证短语是不同的。"; "CertificateImportError" = "在证书导入过程中发生了一个错误。"; +"CertificateStorageLimit" = "証明書の保存上限を超えました。ログイン画面から削除してください。"; // Widget Status "Draft" = "草稿"; diff --git a/AgileWorks/AgileWorks/Strings/Chinese-Traditional.strings b/AgileWorks/AgileWorks/Strings/Chinese-Traditional.strings index 32cafea375d81a1f46226157da2b471034a89504..583c9aac6df414eff864489093cb6cab06c3573b 100644 --- a/AgileWorks/AgileWorks/Strings/Chinese-Traditional.strings +++ b/AgileWorks/AgileWorks/Strings/Chinese-Traditional.strings @@ -104,6 +104,7 @@ "CertificateImportSuccess" = "輸入的客戶證書。"; "IncorrectPassphrase" = "通行證短語是不同的。"; "CertificateImportError" = "在證書導入過程中發生了一個錯誤。"; +"CertificateStorageLimit" = "証明書の保存上限を超えました。ログイン画面から削除してください。"; // Widget Status "Draft" = "草稿"; diff --git a/AgileWorks/AgileWorks/Strings/English.strings b/AgileWorks/AgileWorks/Strings/English.strings index 91107a41618cfb8b63f7f80bd005a3ed131043e5..1aa603084809c062ffbae493d2f75da068c72b08 100644 --- a/AgileWorks/AgileWorks/Strings/English.strings +++ b/AgileWorks/AgileWorks/Strings/English.strings @@ -104,6 +104,7 @@ "CertificateImportSuccess" = "Client certificate imported."; "IncorrectPassphrase" = "The passphrase is different."; "CertificateImportError" = "An error occurred during certificate import."; +"CertificateStorageLimit" = "証明書の保存上限を超えました。ログイン画面から削除してください。"; // Widget Status "Draft" = "Draft"; diff --git a/AgileWorks/AgileWorks/Strings/Japanese.strings b/AgileWorks/AgileWorks/Strings/Japanese.strings index 5219e3cbb1ea6b136263d680437d6a5340abb686..9a7dbfc36191be2cbd9fd7004cc58620853341be 100644 --- a/AgileWorks/AgileWorks/Strings/Japanese.strings +++ b/AgileWorks/AgileWorks/Strings/Japanese.strings @@ -104,6 +104,7 @@ "CertificateImportSuccess" = "クライアント証明書をインポートしました。"; "IncorrectPassphrase" = "パスフレーズが違います。"; "CertificateImportError" = "証明書のインポート中にエラーが発生しました。"; +"CertificateStorageLimit" = "証明書の保存上限を超えました。ログイン画面から削除してください。"; // Widget Status "Draft" = "下書き"; diff --git a/AgileWorks/AgileWorks/Strings/en.lproj/Localizable.strings b/AgileWorks/AgileWorks/Strings/en.lproj/Localizable.strings index c2874e34b6b436b87012ed152fc9568efb66d660..eced4347953aa7dffd672194bf8862f3b6030b75 100644 --- a/AgileWorks/AgileWorks/Strings/en.lproj/Localizable.strings +++ b/AgileWorks/AgileWorks/Strings/en.lproj/Localizable.strings @@ -104,6 +104,7 @@ "CertificateImportSuccess" = "Client certificate imported."; "IncorrectPassphrase" = "The passphrase is different."; "CertificateImportError" = "An error occurred during certificate import."; +"CertificateStorageLimit" = "証明書の保存上限を超えました。ログイン画面から削除してください。"; // Widget Status "Draft" = "Draft"; diff --git a/AgileWorks/AgileWorks/Strings/ja.lproj/Localizable.strings b/AgileWorks/AgileWorks/Strings/ja.lproj/Localizable.strings index 2d54811b12314180f78ffcad8fe1aee295de87dd..0575be7d6041c31b04237a371c1813382c6c48ea 100644 --- a/AgileWorks/AgileWorks/Strings/ja.lproj/Localizable.strings +++ b/AgileWorks/AgileWorks/Strings/ja.lproj/Localizable.strings @@ -104,6 +104,7 @@ "CertificateImportSuccess" = "クライアント証明書をインポートしました。"; "IncorrectPassphrase" = "パスフレーズが違います。"; "CertificateImportError" = "証明書のインポート中にエラーが発生しました。"; +"CertificateStorageLimit" = "証明書の保存上限を超えました。ログイン画面から削除してください。"; // Widget Status "Draft" = "下書き"; diff --git a/AgileWorks/AgileWorks/Strings/zh-Hans.lproj/Localizable.strings b/AgileWorks/AgileWorks/Strings/zh-Hans.lproj/Localizable.strings index 55c49a32e2035efed42b9127d7305890e5bf57e3..8861234e81e6d177a0cb55ce5d99d4a4f2509b47 100644 --- a/AgileWorks/AgileWorks/Strings/zh-Hans.lproj/Localizable.strings +++ b/AgileWorks/AgileWorks/Strings/zh-Hans.lproj/Localizable.strings @@ -104,6 +104,7 @@ "CertificateImportSuccess" = "输入的客户证书。"; "IncorrectPassphrase" = "通行证短语是不同的。"; "CertificateImportError" = "在证书导入过程中发生了一个错误。"; +"CertificateStorageLimit" = "証明書の保存上限を超えました。ログイン画面から削除してください。"; // Widget Status "Draft" = "草稿"; diff --git a/AgileWorks/AgileWorks/Strings/zh-Hant.lproj/Localizable.strings b/AgileWorks/AgileWorks/Strings/zh-Hant.lproj/Localizable.strings index a818da875174b5ffe9bc8460feeccf1bd670c21e..3568da24b98ff5e6bd0ab462ed1230fd10469839 100644 --- a/AgileWorks/AgileWorks/Strings/zh-Hant.lproj/Localizable.strings +++ b/AgileWorks/AgileWorks/Strings/zh-Hant.lproj/Localizable.strings @@ -104,6 +104,7 @@ "CertificateImportSuccess" = "輸入的客戶證書。"; "IncorrectPassphrase" = "通行證短語是不同的。"; "CertificateImportError" = "在證書導入過程中發生了一個錯誤。"; +"CertificateStorageLimit" = "証明書の保存上限を超えました。ログイン画面から削除してください。"; // Widget Status "Draft" = "草稿"; diff --git a/AgileWorks/AgileWorks/WebView/View/WebViewController.swift b/AgileWorks/AgileWorks/WebView/View/WebViewController.swift index 008af00dc4e616ea9c7863dea21bdadbb6904e20..6fdb2824f85b91500ff0ffa0f5d6cc51dd899b16 100644 --- a/AgileWorks/AgileWorks/WebView/View/WebViewController.swift +++ b/AgileWorks/AgileWorks/WebView/View/WebViewController.swift @@ -603,7 +603,7 @@ extension WebViewController: WKNavigationDelegate { private func download(url: URL, saveAs fileName: String) { var request = downloadRequest(url: url) var tempPath: URL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) - Session.shared.dataTask(with: request) { data, res, err in + Session.shared(serverNumber: nil).dataTask(with: request) { data, res, err in guard let data = data, err == nil else { log.e(err) return diff --git a/AgileWorks/Common/DataStore/CertificateDataStore.swift b/AgileWorks/Common/DataStore/CertificateDataStore.swift index 45f08c40f30a16b7ce3db51ac18a628564b77e6f..19f4f3bd4239999331668868a43da49a8f4f71b7 100644 --- a/AgileWorks/Common/DataStore/CertificateDataStore.swift +++ b/AgileWorks/Common/DataStore/CertificateDataStore.swift @@ -17,27 +17,40 @@ let kClientCertLabel = "ClientCert" /// その実装に用いている `Keychain` は `kSecIdentity` の読み書きに対応していないため別途定義。 class CertificateDataStore { /// クライアント証明書を読み込む。取得できなければ `nil` 。 - func readClientCertificate() -> SecIdentity? { - return readIdentity(label: kClientCertLabel) + func readClientCertificate(label: String?) -> SecIdentity? { + guard let label else { + return nil + } + return readIdentity(label: label) + } + func readAllClientCertificate() -> [NSDictionary]? { + return readAllIdentity() } /// クライアント証明書を利用した `URLCredential` (セッション内で有効) を取得する。取得できなければ `nil`。 - func getClientCertificateSessionCredential() -> URLCredential? { - if let identity = readClientCertificate() { + func getClientCertificateSessionCredential(label: String?) -> URLCredential? { + if let identity = readClientCertificate(label: label) { return sessionCredential(identity: identity) } else { return nil } } - /// クライアント証明書を書き込む。 - func writeClientCertificate(identity: SecIdentity) { - writeIdentity(label: kClientCertLabel, identity: identity) + //証明書の書き込み + func writeNewClientCertificate(identity: SecIdentity) -> String? { + let labelID = serchLabelID() + if let labelID = labelID { + var newLabel = kClientCertLabel + labelID + writeIdentity(label: newLabel, identity: identity) + } else { + log.error("Certificate storage limit exceeded") + } + return labelID } /// クライアント証明書を削除する。 - func removeClientCertificate() { - removeIdentity(label: kClientCertLabel) + func removeClientCertificate(label: String) { + removeIdentity(label: label) } private func readIdentity(label: String) -> SecIdentity? { @@ -69,6 +82,36 @@ class CertificateDataStore { } } + //証明書リスト呼び出し + private func readAllIdentity() -> [NSDictionary]? { + let query: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + //kSecAttrLabel as String: label, + kSecReturnRef as String: true, + kSecReturnPersistentRef as String: true, + kSecMatchLimit as String: kSecMatchLimitAll + ] + var result: CFTypeRef? = nil + let status = SecItemCopyMatching(query as CFDictionary, &result) + switch status { + case errSecSuccess: + if let result = result { + let array = (result as? NSArray) as? [NSDictionary] + return array + } else { + log.error("unexpected nil result") + return nil + } + + case errSecItemNotFound: + return nil + + default: + log.error("keychain reading error: OSStatus = \(status)") + return nil + } + } + private func writeIdentity(label: String, identity: SecIdentity) { let query: [String: Any] = [ kSecValueRef as String: identity, @@ -80,6 +123,28 @@ class CertificateDataStore { } } + private func serchLabelID() -> String? { + var labelID = 0 + while labelID < 20 { + let label = kClientCertLabel + String(labelID) + let query: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: label, + kSecReturnRef as String: true, + kSecReturnPersistentRef as String: true + ] + var result: CFTypeRef? = nil + let status = SecItemCopyMatching(query as CFDictionary, &result) + switch status { + case errSecItemNotFound: + return String(labelID) + default: + labelID += 1 + } + } + return nil + } + private func removeIdentity(label: String) { let query: [String: Any] = [ kSecClass as String: kSecClassIdentity, @@ -95,3 +160,4 @@ class CertificateDataStore { return URLCredential(identity: identity, certificates: nil, persistence: .forSession) } } + diff --git a/AgileWorks/Common/DataStore/KeychainDataStore.swift b/AgileWorks/Common/DataStore/KeychainDataStore.swift index cd7b89efbe48befa4d0838f4cde456d7c7336c11..dd6a43efa4943449ab228b881d89116e4ff62d52 100644 --- a/AgileWorks/Common/DataStore/KeychainDataStore.swift +++ b/AgileWorks/Common/DataStore/KeychainDataStore.swift @@ -20,6 +20,7 @@ final class KeychainDataStore: DataStoreProtocol { private let kDeviceID: String = "DeviceID" private let kSessionID: String = "SessionID" private let kServerName: String = "ServerName" + private let kCertificateLabel: String = "CertificateLabel" // アクセストークンの書き込み func writeAccessToken(accessToken: String) { @@ -186,6 +187,20 @@ final class KeychainDataStore: DataStoreProtocol { removeKeychainValue(key: kServerName) } + func writeCertificateLabel(label: String) { + setKeychainValue(key: kCertificateLabel, value: label) + } + + func readCertificateLabel(serverNumber: Int? = nil) -> String? { + if let serverNumber = serverNumber { + return getKeychainValue(key: kCertificateLabel, serverNumber: serverNumber) + } + return getKeychainValue(key: kCertificateLabel) + } + + func removeCertificateLabel() { + removeKeychainValue(key: kCertificateLabel) + } // KeyChain へ指定の値をセット private func setKeychainValue(key: String, value: String) { let keychain = Keychain(service: service, accessGroup: UserDefaultsDataStore().readGroupId()!) diff --git a/AgileWorks/Common/Utility/URLAuthenticationUtility.swift b/AgileWorks/Common/Utility/URLAuthenticationUtility.swift index cd151b6357cb1ef832408c2d4a570bda95d531a3..3eadaec8e9ccba174e09ba4cacb1acc721a97af3 100644 --- a/AgileWorks/Common/Utility/URLAuthenticationUtility.swift +++ b/AgileWorks/Common/Utility/URLAuthenticationUtility.swift @@ -8,12 +8,13 @@ import Foundation -func handleURLAuthenticationChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { +func handleURLAuthenticationChallenge(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void, serverNumber: Int? = nil) { var handled = false switch challenge.protectionSpace.authenticationMethod { case NSURLAuthenticationMethodClientCertificate: - if let credential = CertificateDataStore().getClientCertificateSessionCredential() { + var certificateLabel = KeychainDataStore().readCertificateLabel(serverNumber: serverNumber) + if let credential = CertificateDataStore().getClientCertificateSessionCredential(label: certificateLabel) { completionHandler(.useCredential, credential) handled = true } diff --git a/AgileWorks/Common/WebClient/Session.swift b/AgileWorks/Common/WebClient/Session.swift index da0f712ec3c119f40eb09df4fef1398c50e62e7f..bb3886b6cfe4793ef4fda2f1b7b0d5e26d70462e 100644 --- a/AgileWorks/Common/WebClient/Session.swift +++ b/AgileWorks/Common/WebClient/Session.swift @@ -17,25 +17,29 @@ enum SessionError: Error { class Session { private static let kRetryCount = 3 - private static let privateShared = URLSession(configuration: .default, delegate: SessionDelegate(), delegateQueue: nil) + //private static let privateShared = URLSession(configuration: .default, delegate: SessionDelegate(), delegateQueue: nil) private static var retryCount = kRetryCount - class var shared: URLSession { + class func shared(serverNumber: Int? = nil) -> URLSession { + var sessionDelegate = SessionDelegate() + + sessionDelegate.serverNumber = serverNumber + let privateShared = URLSession(configuration: .default, delegate: sessionDelegate, delegateQueue: nil) return privateShared } private class SessionDelegate: NSObject, URLSessionDelegate { + var serverNumber: Int? = nil func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - handleURLAuthenticationChallenge(challenge, completionHandler: completionHandler) + handleURLAuthenticationChallenge(challenge, completionHandler: completionHandler, serverNumber: serverNumber) } } @discardableResult - class func send(_ request: T, completion: @escaping (Result) -> Void) -> URLSessionTask? { + class func send(serverNumber: Int? = nil, _ request: T, completion: @escaping (Result) -> Void) -> URLSessionTask? { let req = request.request log.debug("Session Request: \(String(describing: req.url))\nHTTPMethod: \(String(describing: req.httpMethod) )\nHeaders: \(String(describing: req.allHTTPHeaderFields))\nHTTPBody: \(String(data: req.httpBody ?? Data(), encoding: .utf8)!)") - - let task = shared.dataTask(with: req) { data, response, error in + let task = shared(serverNumber: serverNumber).dataTask(with: req) { data, response, error in let result: Result let res = response as? HTTPURLResponse diff --git a/AgileWorks/ShareExtension/View/ShareViewController.swift b/AgileWorks/ShareExtension/View/ShareViewController.swift index b0be7d8afcd58ac7334fe8707ca2474f531f158a..e0086a8753e3a29121d7d2a045b72c35b794fa64 100644 --- a/AgileWorks/ShareExtension/View/ShareViewController.swift +++ b/AgileWorks/ShareExtension/View/ShareViewController.swift @@ -47,9 +47,19 @@ class ShareViewController: UITableViewController { @IBAction func onImportButtonTapped(_ sender: UIBarButtonItem) { importCertificateFromContext() { identity, error in if let identity = identity { - CertificateDataStore().writeClientCertificate(identity: identity) - self.alertInUiThread(message: getDisplayString(key: "CertificateImportSuccess", comment: "")) { - self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + //書き込み + let result = CertificateDataStore().writeNewClientCertificate(identity: identity) + + if let _ = result { + //成功 + self.alertInUiThread(message: getDisplayString(key: "CertificateImportSuccess", comment: "")) { + self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + } + } else { + //保存上限オーバー + self.alertInUiThread(message: getDisplayString(key: "CertificateStorageLimit", comment: "")) { + self.extensionContext?.cancelRequest(withError: error ?? ShareExtensionError.genericError) + } } } else { if let error = error as NSError?,