diff --git a/AgileWorks/AgileWorks.xcodeproj/project.pbxproj b/AgileWorks/AgileWorks.xcodeproj/project.pbxproj index 12a7cb625d84c7c3b131691e8a77b366f0cf5019..27e433d4d6c8adddc201f6096f0d2f613cbbaee1 100644 --- a/AgileWorks/AgileWorks.xcodeproj/project.pbxproj +++ b/AgileWorks/AgileWorks.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -185,6 +185,8 @@ C57DCAB027C8AEFC000A2ABC /* ProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57DCAAF27C8AEFC000A2ABC /* ProfileTableViewController.swift */; }; C5848EB827BC8EBC00AA796D /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5848EB727BC8EBB00AA796D /* MenuViewController.swift */; }; C5848EBA27BCE0F100AA796D /* MenuTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5848EB927BCE0F100AA796D /* MenuTableViewController.swift */; }; + C58DE59F2B2717B40004C4E6 /* OldServerCertificateDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58DE59D2B2717B40004C4E6 /* OldServerCertificateDataStore.swift */; }; + C58DE5A02B2717B40004C4E6 /* OldServerKeychainDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58DE59E2B2717B40004C4E6 /* OldServerKeychainDataStore.swift */; }; C599740827D992A5006F5AAC /* OpenLicenseTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C599740727D992A5006F5AAC /* OpenLicenseTableViewController.swift */; }; C599740B27D992E9006F5AAC /* OpenLicenseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C599740A27D992E9006F5AAC /* OpenLicenseTableViewCell.swift */; }; C599740D27D9E634006F5AAC /* OpenLicenseDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C599740C27D9E633006F5AAC /* OpenLicenseDetailViewController.swift */; }; @@ -409,6 +411,8 @@ C57DCAAF27C8AEFC000A2ABC /* ProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTableViewController.swift; sourceTree = ""; }; C5848EB727BC8EBB00AA796D /* MenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; }; C5848EB927BCE0F100AA796D /* MenuTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuTableViewController.swift; sourceTree = ""; }; + C58DE59D2B2717B40004C4E6 /* OldServerCertificateDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OldServerCertificateDataStore.swift; sourceTree = ""; }; + C58DE59E2B2717B40004C4E6 /* OldServerKeychainDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OldServerKeychainDataStore.swift; sourceTree = ""; }; C599740727D992A5006F5AAC /* OpenLicenseTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenLicenseTableViewController.swift; sourceTree = ""; }; C599740A27D992E9006F5AAC /* OpenLicenseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenLicenseTableViewCell.swift; sourceTree = ""; }; C599740C27D9E633006F5AAC /* OpenLicenseDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenLicenseDetailViewController.swift; sourceTree = ""; }; @@ -716,6 +720,8 @@ BD0231F2242B6E06000A95BB /* DataStore */ = { isa = PBXGroup; children = ( + C58DE59D2B2717B40004C4E6 /* OldServerCertificateDataStore.swift */, + C58DE59E2B2717B40004C4E6 /* OldServerKeychainDataStore.swift */, BD5061B3242866780014F3FA /* Configuration.swift */, BD928CCD2407500400ED04C2 /* UserDefaultsDataStore.swift */, BDEA2CD4246CDAEF00D3E15F /* KeychainDataStore.swift */, @@ -1532,11 +1538,13 @@ files = ( 764D0DB5257F450500AB6617 /* CertificateDataStore.swift in Sources */, C5E5A4C827853BEF00668C7D /* QRCodeReader.swift in Sources */, + C58DE59F2B2717B40004C4E6 /* OldServerCertificateDataStore.swift in Sources */, BD6C1AE424A471070057756F /* PutDeviceEndpoint.swift in Sources */, BDA1831323F3FD7F00C9A6DD /* AppDelegate.swift in Sources */, BD3E90862474F50B00B449A7 /* IntExtensions.swift in Sources */, C5848EB827BC8EBC00AA796D /* MenuViewController.swift in Sources */, BDA74C5624501917000D4351 /* Log.swift in Sources */, + C58DE5A02B2717B40004C4E6 /* OldServerKeychainDataStore.swift in Sources */, 85X3ZXEG18PWU7J17GNGF3UG /* SplashPresenter.swift in Sources */, KQWCT9WOHFNQBPP7I7C3OL6Q /* SplashViewController.swift in Sources */, BDBBF838243C9EA300EEB25D /* GetApprovalsEndpoint.swift in Sources */, diff --git a/AgileWorks/AgileWorks/AgileWorks-Production.entitlements b/AgileWorks/AgileWorks/AgileWorks-Production.entitlements index 002355ec729a449df0ca46517ada98541abb10d8..f691bfa5310b3166e5abad87bf86e233bd846951 100644 --- a/AgileWorks/AgileWorks/AgileWorks-Production.entitlements +++ b/AgileWorks/AgileWorks/AgileWorks-Production.entitlements @@ -15,6 +15,7 @@ $(AppIdentifierPrefix)jp.atled.agileworks2 $(AppIdentifierPrefix)jp.atled.agileworks3 $(AppIdentifierPrefix)jp.atled.agileworks4 + $(AppIdentifierPrefix)jp.atled.agileworks diff --git a/AgileWorks/AgileWorks/AgileWorksRelease-Production.entitlements b/AgileWorks/AgileWorks/AgileWorksRelease-Production.entitlements index 002355ec729a449df0ca46517ada98541abb10d8..f691bfa5310b3166e5abad87bf86e233bd846951 100644 --- a/AgileWorks/AgileWorks/AgileWorksRelease-Production.entitlements +++ b/AgileWorks/AgileWorks/AgileWorksRelease-Production.entitlements @@ -15,6 +15,7 @@ $(AppIdentifierPrefix)jp.atled.agileworks2 $(AppIdentifierPrefix)jp.atled.agileworks3 $(AppIdentifierPrefix)jp.atled.agileworks4 + $(AppIdentifierPrefix)jp.atled.agileworks diff --git a/AgileWorks/AgileWorks/Splash/View/SplashViewController.swift b/AgileWorks/AgileWorks/Splash/View/SplashViewController.swift index efa76c5ae92298f7edf4b42f70b5fdb140bd13d0..de7daeaa90ec3a6f5bfaa234b214fe7256e2914b 100644 --- a/AgileWorks/AgileWorks/Splash/View/SplashViewController.swift +++ b/AgileWorks/AgileWorks/Splash/View/SplashViewController.swift @@ -47,6 +47,9 @@ class SplashViewController: UIViewController { private func makeServiceCall() { activityIndicator.startAnimating() + // サーバ情報引き継ぎ + updateServerSettings() + let serverList = UserDefaultsDataStore().readServerList() if !serverList.isEmpty { UserDefaultsDataStore().setGroupId(serverNumber: serverList.first!) @@ -104,6 +107,46 @@ class SplashViewController: UIViewController { } } } + + // サーバ切り替え対応前アプリのサーバ情報引き継ぎ + private func updateServerSettings() { + // 旧バージョン時のログイン情報を引き継ぐ + guard OldServerKeychainDataStore().readOAuthState() != nil else { + return + } + // 旧Keychainに保存されてる内容を、0番目のKeychainにコピーして、旧Keychain情報をクリアする + let keychain = KeychainDataStore() + let oldKeychain = OldServerKeychainDataStore() + keychain.writeAccessToken(accessToken: oldKeychain.readAccessToken()!) + keychain.writeOAuthState(authState: oldKeychain.readOAuthState()!) + keychain.writeLanguage(language: oldKeychain.readLanguage()!) + keychain.writeServerURL(serverURL: oldKeychain.readServerURL()!) + keychain.writeContextPath(contextPath: oldKeychain.readContextPath()!) + keychain.writeDeviceID(deviceID: oldKeychain.readDeviceID()!) + keychain.writeSessionID(sessionID: oldKeychain.readSessionID()!) + // 旧情報クリア + oldKeychain.removeAccessToken() + oldKeychain.removeOAuthState() + oldKeychain.removeLanguage() + oldKeychain.removeServerURL() + oldKeychain.removeContextPath() + oldKeychain.removeDeviceID() + oldKeychain.removeSessionID() + // サーバリストの追加 + UserDefaultsDataStore().addServerList() + // ライセンス情報は同意済みのため、現バージョンをセット + writeAcceptedLicenseVersionToCurrentVersion() + // クライアント証明書の引き継ぎ + guard OldServerCertificateDataStore().readClientCertificate() != nil else { + return + } + let label = CertificateDataStore().writeNewClientCertificate(identity: OldServerCertificateDataStore().readClientCertificate()!) + // 旧バージョンでクライアント証明書が設定されている=クライアント証明書を使用しての通信を実施しているので + // その情報も引き継ぐ + KeychainDataStore().writeCertificateLabel(label: "ClientCert" + label!) + // 証明書情報削除 + OldServerCertificateDataStore().removeClientCertificate() + } } extension SplashViewController: SplashView { diff --git a/AgileWorks/Common/DataStore/OldServerCertificateDataStore.swift b/AgileWorks/Common/DataStore/OldServerCertificateDataStore.swift new file mode 100644 index 0000000000000000000000000000000000000000..6b4ce7cab92bac995828eb5c370a2ec70fafd4d6 --- /dev/null +++ b/AgileWorks/Common/DataStore/OldServerCertificateDataStore.swift @@ -0,0 +1,97 @@ +// +// OldServerCertificateDataStore.swift +// AgileWorks +// +// Created by Gk40002148 on 2023/12/06. +// Copyright © 2023 ATLED CORP. All rights reserved. +// + +import Foundation + +// クライアント証明書格納用ラベル +//let clientCertLabel = "ClientCert" + +/// 証明書情報のデータストア。 +/// +/// `KeychainDataStore` と同様にキーチェーンへのアクセスを行うが、`KeychainDataStore` が +/// その実装に用いている `Keychain` は `kSecIdentity` の読み書きに対応していないため別途定義。 +class OldServerCertificateDataStore { + /// クライアント証明書を読み込む。取得できなければ `nil` 。 + func readClientCertificate() -> SecIdentity? { + return readIdentity(label: kClientCertLabel) + } + + /// クライアント証明書を利用した `URLCredential` (セッション内で有効) を取得する。取得できなければ `nil`。 + func getClientCertificateSessionCredential() -> URLCredential? { + if let identity = readClientCertificate() { + return sessionCredential(identity: identity) + } else { + return nil + } + } + + /// クライアント証明書を書き込む。 + func writeClientCertificate(identity: SecIdentity) { + writeIdentity(label: kClientCertLabel, identity: identity) + } + + /// クライアント証明書を削除する。 + func removeClientCertificate() { + removeIdentity(label: kClientCertLabel) + } + + private func readIdentity(label: String) -> SecIdentity? { + 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 errSecSuccess: + if let result = result { + let dic = result as! [String: Any] + let identity = dic[kSecValueRef as String] as! SecIdentity + return identity + } 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, + kSecAttrLabel as String: label, + ] + let status = SecItemAdd(query as CFDictionary, nil) + if status != errSecSuccess { + log.error("keychain writing error: OSStatus = \(status)") + } + } + + private func removeIdentity(label: String) { + let query: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: label + ] + let status = SecItemDelete(query as CFDictionary) + if status != errSecSuccess { + log.error("keychain writing error: OSStatus = \(status)") + } + } + + private func sessionCredential(identity: SecIdentity) -> URLCredential { + return URLCredential(identity: identity, certificates: nil, persistence: .forSession) + } +} diff --git a/AgileWorks/Common/DataStore/OldServerKeychainDataStore.swift b/AgileWorks/Common/DataStore/OldServerKeychainDataStore.swift new file mode 100644 index 0000000000000000000000000000000000000000..a6e3446cbc2445e5a8dac8e0f06367b5f84954c0 --- /dev/null +++ b/AgileWorks/Common/DataStore/OldServerKeychainDataStore.swift @@ -0,0 +1,165 @@ +// +// OldServerKeychainDataStore.swift +// AgileWorks +// +// Created by Gk40002148 on 2023/12/06. +// Copyright © 2023 ATLED CORP. All rights reserved. +// + +import AppAuth +import KeychainAccess +import UIKit + +final class OldServerKeychainDataStore: DataStoreProtocol { + private let service = "AgileWorks" + private let groupId = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String + Configuration.shared.awShareBundleIdentifier + private let kAccessToken: String = "AccessToken" + private let kOAuthState: String = "OAuthState" + private let kLanguage: String = "Localizable" + private let kServerURL: String = "ServerURL" + private let kContextPath: String = "ContextPath" + private let kDeviceID: String = "DeviceID" + private let kSessionID: String = "SessionID" + + // アクセストークンの書き込み + func writeAccessToken(accessToken: String) { + setKeychainValue(key: kAccessToken, value: accessToken) + } + + // アクセストークンの読み込み + func readAccessToken() -> String? { + return getKeychainValue(key: kAccessToken) + } + + // アクセストークンの削除 + func removeAccessToken() { + removeKeychainValue(key: kAccessToken) + } + + // 認可ステータスの書き込み + func writeOAuthState(authState: OIDAuthState) { + let keychain = Keychain(service: service, accessGroup: groupId) + guard let object = try? NSKeyedArchiver.archivedData(withRootObject: authState, requiringSecureCoding: true) else { + return + } + keychain[data: kOAuthState] = object + } + + // 認可ステータスの読み込み + func readOAuthState() -> OIDAuthState? { + let keychain = Keychain(service: service, accessGroup: groupId) + if let object = keychain[data: kOAuthState] { + guard let authState = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(object) as? OIDAuthState else { + return nil + } + return authState + } + return nil + } + + // 認可ステータスの削除 + func removeOAuthState() { + removeKeychainValue(key: kOAuthState) + } + + // 言語設定の書き込み + func writeLanguage(language: String) { + setKeychainValue(key: kLanguage, value: language) + } + + // 言語設定の読み込み + func readLanguage() -> String? { + return getKeychainValue(key: kLanguage) + } + + // 言語設定の削除 + func removeLanguage() { + removeKeychainValue(key: kLanguage) + } + + // サーバーURLの書き込み + func writeServerURL(serverURL: String) { + setKeychainValue(key: kServerURL, value: serverURL) + } + + // サーバーURLの読み込み + func readServerURL() -> String? { + return getKeychainValue(key: kServerURL) + } + + // サーバーURLの削除 + func removeServerURL() { + removeKeychainValue(key: kServerURL) + } + + // コンテキストパスの書き込み + func writeContextPath(contextPath: String) { + setKeychainValue(key: kContextPath, value: contextPath) + } + + // コンテキストパスの読み込み + func readContextPath() -> String? { + return getKeychainValue(key: kContextPath) + } + + // コンテキストパスの削除 + func removeContextPath() { + removeKeychainValue(key: kContextPath) + } + + // デバイスIDの書き込み + func writeDeviceID(deviceID: Int) { + setKeychainValue(key: kDeviceID, value: deviceID.description) + } + + // デバイスIDの読み込み + func readDeviceID() -> Int? { + guard let deviceIDString = getKeychainValue(key: kDeviceID) else { + return nil + } + guard let deviceID = Int(deviceIDString) else { + return nil + } + return deviceID + } + + // デバイスIDの削除 + func removeDeviceID() { + removeKeychainValue(key: kDeviceID) + } + + // セッションIDの書き込み + func writeSessionID(sessionID: String) { + setKeychainValue(key: kSessionID, value: sessionID) + } + + // セッションIDの読み込み + func readSessionID() -> String? { + return getKeychainValue(key: kSessionID) + } + + // セッションIDの削除 + func removeSessionID() { + removeKeychainValue(key: kSessionID) + } + + // KeyChain へ指定の値をセット + private func setKeychainValue(key: String, value: String) { + let keychain = Keychain(service: service, accessGroup: groupId) + keychain[key] = value + } + // KeyChain から指定の値を取得 + private func getKeychainValue(key: String) -> String? { + let keychain = Keychain(service: service, accessGroup: groupId) + return keychain[key] + } + // KeyChain から指定の値を削除 + private func removeKeychainValue(key: String) { + let keychain = Keychain(service: service, accessGroup: groupId) + do { + try keychain.remove(key) + } catch { + log.e(error) + } + } +}