diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb460e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..9157eb5 --- /dev/null +++ b/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "mew-wallet-ios-keychain", + platforms: [ + .iOS(.v10) + ], + products: [ + .library( + name: "mew-wallet-ios-keychain", + targets: ["mew-wallet-ios-keychain"]), + ], + targets: [ + .target( + name: "mew-wallet-ios-keychain", + dependencies: [], + path: "Sources") + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb92d61 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# mew-wallet-ios-keychain + +A description of this package. diff --git a/Sources/Concrete/KeychainImplementation.swift b/Sources/Concrete/KeychainImplementation.swift new file mode 100644 index 0000000..2c90797 --- /dev/null +++ b/Sources/Concrete/KeychainImplementation.swift @@ -0,0 +1,305 @@ +// +// KeychainImplementation.swift +// mew-wallet-ios-keychain +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import Foundation +import LocalAuthentication + +public class KeychainImplementation: Keychain { + public let accessGroup: String? + + public init(accessGroup: String?) { + self.accessGroup = accessGroup + } + + public func save(_ record: KeychainRecord) throws { + try self.validateNotFound(record: record, context: nil) + let query = try KeychainQueryFactory.save(record, accessGroup: self.accessGroup) + + do { + try query.execute().get() + } catch KeychainQueryError.invalidRecord { + throw KeychainError.general(message: "Couldn't save record. It's possible that the access control you have provided isn't supported on this OS and/or hardware.") + } catch KeychainQueryError.missingEntitlement { + throw KeychainError.general(message: "Couldn't save record. Missing entitlement.") + } catch KeychainQueryError.duplicateItem { + throw KeychainError.general(message: "Couldn't save record. Duplicate item.") + } catch let KeychainQueryError.other(status: status) { + throw KeychainError.general(message: "Couldn't save record. OSStatus: \(status). Record: \(record)") + } + } + + public func update(_ record: KeychainRecord) throws { + guard case KeychainRecord.data = record else { + throw KeychainError.general(message: "Couldn't update record. Not supported.") + } + do { + try self.validateNotFound(record: record, context: nil) + try self.save(record) + return + } catch {} + + let query = try KeychainQueryFactory.update(record, accessGroup: self.accessGroup) + do { + try query.execute().get() + } catch KeychainQueryError.invalidRecord { + throw KeychainError.general(message: "Couldn't update record. It's possible that the access control you have provided isn't supported on this OS and/or hardware.") + } catch KeychainQueryError.missingEntitlement { + throw KeychainError.general(message: "Couldn't update record. Missing entitlement.") + } catch KeychainQueryError.duplicateItem { + throw KeychainError.general(message: "Couldn't update record. Duplicate item.") + } catch let KeychainQueryError.other(status: status) { + throw KeychainError.general(message: "Couldn't update record. OSStatus: \(status). Record: \(record)") + } + } + + public func load(_ record: KeychainRecord, context: LAContext?) throws -> KeychainRecord { + do { + switch record { + case .key: + let query: KeychainQuery = KeychainQueryFactory.load(record, accessGroup: self.accessGroup, context: context) + let key = try query.execute().get() + guard let updated = record.withUpdated(key: key) else { + throw KeychainError.general(message: "Internal error") + } + return updated + case .data: + let query: KeychainQuery = KeychainQueryFactory.load(record, accessGroup: self.accessGroup, context: context) + let data = try query.execute().get() + guard let updated = record.withUpdated(data: data) else { + throw KeychainError.general(message: "Internal error") + } + return updated + } + } catch let error as KeychainQueryError { + throw KeychainError.general(message: "Couldn't get data for record: \(record). Status: \(error.status)") + } catch { + throw KeychainError.general(message: "Couldn't get data for record: \(record)") + } + } + + public func delete(_ record: KeychainRecord) throws { + let query = KeychainQueryFactory.delete(record, accessGroup: self.accessGroup) + do { + try query.execute().get() + } catch KeychainQueryError.invalidRecord { + throw KeychainError.general(message: "Couldn't delete key. It's possible that the access control you have provided isn't supported on this OS and/or hardware.") + } catch let error as KeychainQueryError { + throw KeychainError.general(message: "Couldn't delete key. OSStatus: \(error.status)") + } catch { + throw KeychainError.general(message: "Couldn't delete key") + } + } + + public func generate(keys: KeychainKeypair, context: LAContext) throws -> KeychainKeypair { + do { + try self.validateNotFound(record: keys.prv, context: context) + try self.validateNotFound(record: keys.pub, context: context) + let query = try KeychainQueryFactory.generate(keys, accessGroup: self.accessGroup, context: context) + let generatedKeys = try query.execute().get() + + guard let updated = keys.withUpdated(keys: generatedKeys) else { + throw KeychainError.inconcistency(message: "Can't generate keys") + } + if !keys.secureEnclave { + try self.save(updated.prv) + } + try self.save(updated.pub) + return updated + } catch { + try? self.delete(keys.prv) + try? self.delete(keys.pub) + throw error + } + } + + public func verifySecureEnclave(context: LAContext) throws { + let uuid = UUID().uuidString + guard let keypair = KeychainKeypair( + prv: .key(key: nil, label: "prv-\(uuid)"), + pub: .key(key: nil, label: "pub-\(uuid)") + ) else { + throw KeychainError.inconcistency(message: "Can't verify keys") + } + + + try self.delete(keypair.prv) + try self.delete(keypair.pub) + + let range: ClosedRange = 0...255 + let randomData = Data([UInt8.random(in: range), + UInt8.random(in: range), + UInt8.random(in: range), + UInt8.random(in: range)]) + + _ = try self.generate(keys: keypair, context: context) + defer { + try? self.delete(keypair.prv) + try? self.delete(keypair.pub) + } + + let encrypted = try self.encrypt(pub: keypair.pub, + digest: .data(data: randomData, label: nil, account: nil), + context: context) + let decrypted = try self.decrypt(prv: keypair.prv, + encrypted: encrypted, + context: context) + + guard randomData == decrypted.data else { + throw KeychainError.inconcistency(message: "Can't verify keys") + } + } + + // MARK: - Encryption + + public func encrypt(pub: KeychainRecord, digest: KeychainRecord, context: LAContext) throws -> KeychainRecord { + guard let key = try pub.key ?? self.load(pub, context: context).key else { + throw KeychainError.notFound(message: "Coundn't find a key: \(pub)") + } + guard let data = digest.data else { + throw KeychainError.general(message: "Empty data.") + } + + var error: Unmanaged? + let result = SecKeyCreateEncryptedData(key, .eciesEncryptionCofactorX963SHA256AESGCM, data as CFData, &error) + guard let encrypted = result as Data? else { + if let error = error { + throw KeychainError.fromError(error.takeRetainedValue(), message: "Could not encrypt.") + } else { + throw KeychainError.general(message: "Could not encrypt.") + } + } + guard let updated = digest.withUpdated(data: encrypted) else { + throw KeychainError.general(message: "Internal error") + } + return updated + } + + public func decrypt(prv: KeychainRecord, encrypted: KeychainRecord, context: LAContext) throws -> KeychainRecord { + guard let key = try prv.key ?? self.load(prv, context: context).key else { + throw KeychainError.notFound(message: "Coundn't find a key: \(prv)") + } + + guard let data = encrypted.data else { + throw KeychainError.general(message: "Empty data.") + } + + var error: Unmanaged? + let result = SecKeyCreateDecryptedData(key, .eciesEncryptionCofactorX963SHA256AESGCM, data as CFData, &error) + guard let decrypted = result as Data? else { + if let error = error { + throw KeychainError.fromError(error.takeRetainedValue(), message: "Could not decrypt.") + } else { + throw KeychainError.general(message: "Could not decrypt.") + } + } + guard let updated = encrypted.withUpdated(data: decrypted) else { + throw KeychainError.general(message: "Internal error") + } + return updated + } + + public func encryptAndSave(pub: KeychainRecord, item: KeychainRecord, context: LAContext) throws { + try self.validateNotFound(record: item, context: context) + let encrypted = try self.encrypt(pub: pub, digest: item, context: context) + try self.save(encrypted) + } + + public func loadAndDecrypt(prv: KeychainRecord, item: KeychainRecord, context: LAContext) throws -> KeychainRecord { + let encrypted = try self.load(item, context: context) + return try self.decrypt(prv: prv, encrypted: encrypted, context: context) + } + + public func change(keys: KeychainKeypair, item: KeychainRecord, oldContext: LAContext, newContext: LAContext) throws { + // Verify SecureEnclave + try self.verifySecureEnclave(context: newContext) + + // Generate temporary keypair + var tempKeys = KeychainKeypair(prv: "\(keys.prv.label ?? "")-change", + pub: "\(keys.pub.label ?? "")-change", + secureEnclave: true) + try? self.delete(tempKeys.prv) + try? self.delete(tempKeys.pub) + tempKeys = try self.generate(keys: tempKeys, context: newContext) + + // Load and decrypt old data + let decrypted = try self.loadAndDecrypt(prv: keys.prv, + item: item, + context: oldContext) + + let backupItem: KeychainRecord = .data(data: decrypted.data, + label: "\(decrypted.label ?? "")-change", + account: "\(decrypted.account ?? "")-change") + // Encrypt and save data to have a backup + try self.encryptAndSave(pub: tempKeys.pub, + item: backupItem, + context: newContext) + + // Verify backup + let decryptedBackup = try self.loadAndDecrypt(prv: tempKeys.prv, + item: .data(data: nil, + label: backupItem.label, + account: backupItem.account), + context: newContext) + + guard decrypted.data != nil, decrypted.data == decryptedBackup.data else { + try? self.delete(tempKeys.prv) + try? self.delete(tempKeys.pub) + try? self.delete(backupItem) + throw KeychainError.inconcistency(message: "Can't verify backup") + } + + // Delete old keypair and data + try? self.delete(item) + try? self.delete(keys.prv) + try? self.delete(keys.pub) + + // Generate new keypair + let newKeys = try self.generate(keys: keys, context: newContext) + + // Save data + try self.encryptAndSave(pub: newKeys.pub, + item: decrypted, + context: newContext) + + let newDecrypted = try self.loadAndDecrypt(prv: newKeys.prv, + item: .data(data: nil, + label: item.label, + account: item.account), + context: newContext) + + guard decrypted.data != nil, decrypted.data == newDecrypted.data else { + throw KeychainError.inconcistency(message: "Something went wrong") + } + } + + public func reset() { + let deleteData = KeychainQueryFactory.deleteAll(.data(data: nil, label: nil, account: nil), accessGroup: self.accessGroup) + let deleteKeys = KeychainQueryFactory.deleteAll(.key(key: nil, label: nil), accessGroup: self.accessGroup) + + _ = try? deleteData.execute().get() + _ = try? deleteKeys.execute().get() + } + + // MARK: - Private + + private func validateNotFound(record: KeychainRecord, context: LAContext?) throws { + let loadQuery: KeychainQuery = KeychainQueryFactory.load(record, accessGroup: self.accessGroup, context: context) + do { + _ = try loadQuery.execute().get() + switch record { + case let .data(_, label, account): + throw KeychainError.general(message: "Duplicate item. Label: \(label ?? ""). Account: \(account ?? "")") + case let .key(_, label: label): + throw KeychainError.general(message: "Duplicate key. Label: \(label ?? "")") + } + } catch KeychainQueryError.notFound { + } catch { + throw error + } + } +} diff --git a/Sources/Concrete/KeychainQuery.swift b/Sources/Concrete/KeychainQuery.swift new file mode 100644 index 0000000..17306c3 --- /dev/null +++ b/Sources/Concrete/KeychainQuery.swift @@ -0,0 +1,385 @@ +// +// KeychainQuery.swift +// mew-wallet-ios-keychain +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import Foundation +import LocalAuthentication + +enum KeychainQueryOperation { + case add + case update([String: Any]) + case delete + case load + case generate + case deleteAll + case all +} + +enum KeychainQueryError: Error { + case invalidRecord + case missingEntitlement + case duplicateItem + case notFound + case other(status: OSStatus) + + init(status: OSStatus) { + switch status { + case errSecInvalidRecord: + self = .invalidRecord + case errSecMissingEntitlement: + self = .missingEntitlement + case errSecDuplicateItem: + self = .duplicateItem + case errSecItemNotFound: + self = .notFound + default: + self = .other(status: status) + } + } + + var status: OSStatus { + switch self { + case .invalidRecord: + return errSecInvalidRecord + case .missingEntitlement: + return errSecMissingEntitlement + case .duplicateItem: + return errSecDuplicateItem + case .notFound: + return errSecItemNotFound + case let .other(status): + return status + } + } +} + +class KeychainQuery { + let raw: [String: Any] + let operation: KeychainQueryOperation + + private init(raw: [String: Any], operation: KeychainQueryOperation) { + self.raw = raw + self.operation = operation + } + + // MARK: - Add + + static func add(key: SecKey, label: String?, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecValueRef as String: key, + kSecAttrSynchronizable as String: false + ] + if let label = label { + query[kSecAttrLabel as String] = label + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .add) + } + + static func add(item: Data, label: String?, account: String?, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecValueData as String: item, + kSecAttrSynchronizable as String: false + ] + if let label = label { + query[kSecAttrLabel as String] = label + } + if let account = account { + query[kSecAttrAccount as String] = account + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .add) + } + + // MARK: - Add + + static func update(key: SecKey, label: String?, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrSynchronizable as String: false + ] + if let label = label { + query[kSecAttrLabel as String] = label + } + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .update([kSecValueRef as String: key])) + } + + static func update(item: Data, label: String?, account: String?, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrSynchronizable as String: false + ] + if let label = label { + query[kSecAttrLabel as String] = label + } + if let account = account { + query[kSecAttrAccount as String] = account + } + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .update([kSecValueData as String: item])) + } + + // MARK: - Delete + + static func delete(key label: String?, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrSynchronizable as String: false + ] + if let label = label { + query[kSecAttrLabel as String] = label + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .delete) + } + + static func delete(item label: String?, account: String?, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrSynchronizable as String: false + ] + if let label = label { + query[kSecAttrLabel as String] = label + } + if let account = account { + query[kSecAttrAccount as String] = account + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .delete) + } + + static func deleteAll(keys: Bool, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecAttrSynchronizable as String: false + ] + if keys { + query[kSecClass as String] = kSecClassKey + } else { + query[kSecClass as String] = kSecClassGenericPassword + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .deleteAll) + } + + // MARK: - Load + + static func load(key label: String?, accessGroup: String?, context: LAContext?) -> KeychainQuery { + var query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecReturnRef as String: true, + kSecAttrSynchronizable as String: false + ] + + if let label = label { + query[kSecAttrLabel as String] = label + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + if let context = context { + query[kSecUseAuthenticationContext as String] = context + if !context.isCredentialSet(.applicationPassword) { + query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIAllow + } + } + + return KeychainQuery(raw: query, operation: .load) + } + + static func load(item label: String?, account: String?, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecAttrSynchronizable as String: false + ] + if let label = label { + query[kSecAttrLabel as String] = label + } + if let account = account { + query[kSecAttrAccount as String] = account + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .load) + } + + // Generate + + static func generate(prvLabel: String?, pubLabel: String?, accessGroup: String?, context: LAContext?, secureEnclave: Bool) throws -> KeychainQuery { + /* ========= private ========= */ + var prvQuery: [String: Any] = [ + kSecAttrIsPermanent as String: true, + kSecAttrSynchronizable as String: false + ] + if let label = prvLabel { + prvQuery[kSecAttrLabel as String] = label + } + + if let context = context { + prvQuery[kSecUseAuthenticationContext as String] = context + if !context.isCredentialSet(.applicationPassword) { + prvQuery[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUIAllow + } + } + + if let context = context { + prvQuery[kSecAttrAccessControl as String] = try context.accessControlCreateFlags.createAccessControl() + } else { + prvQuery[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly + } + + /* ========= public ========= */ + var pubQuery: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrAccessible as String: kSecAttrAccessibleAlwaysThisDeviceOnly, + kSecAttrSynchronizable as String: false + ] + if let label = pubLabel { + pubQuery[kSecAttrLabel as String] = label + } + + if let accessGroup = accessGroup { + prvQuery[kSecAttrAccessGroup as String] = accessGroup + pubQuery[kSecAttrAccessGroup as String] = accessGroup + } + + /* ========= combined ========= */ + var query: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecPrivateKeyAttrs as String: prvQuery, + kSecPublicKeyAttrs as String: pubQuery, + kSecAttrKeySizeInBits as String: 256, + kSecAttrSynchronizable as String: false + ] + + if secureEnclave { + query[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave + } + + return KeychainQuery(raw: query, operation: .generate) + } + + // MARK: - All + + static func all(keys: Bool, accessGroup: String?) -> KeychainQuery { + var query: [String: Any] = [ + kSecReturnData as String: false, + kSecReturnAttributes as String: true, + kSecMatchLimit as String: kSecMatchLimitAll, + kSecAttrSynchronizable as String: false + ] + if keys { + query[kSecClass as String] = kSecClassKey + } else { + query[kSecClass as String] = kSecClassGenericPassword + } + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return KeychainQuery(raw: query, operation: .all) + } + + // MARK: - Execute + + func execute() -> Result { + let status: OSStatus + switch self.operation { + case .add: + status = SecItemAdd(self.raw as CFDictionary, nil) + guard status == errSecSuccess else { + return .failure(KeychainQueryError(status: status)) + } + return .success(() as! R) + case let .update(update): + status = SecItemUpdate(self.raw as CFDictionary, update as CFDictionary) + guard status == errSecSuccess else { + return .failure(KeychainQueryError(status: status)) + } + return .success(() as! R) + case .delete: + status = SecItemDelete(self.raw as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + return .failure(KeychainQueryError(status: status)) + } + return .success(() as! R) + case .load: + var raw: CFTypeRef? + status = SecItemCopyMatching(self.raw as CFDictionary, &raw) + guard status == errSecSuccess, raw != nil else { + if raw == nil { + return .failure(.notFound) + } else { + return .failure(KeychainQueryError(status: status)) + } + } + return .success(raw as! R) + case .generate: + var publicSecKey: SecKey? + var privateSecKey: SecKey? + + let status = SecKeyGeneratePair(self.raw as CFDictionary, &publicSecKey, &privateSecKey) + guard status == errSecSuccess, privateSecKey != nil, publicSecKey != nil else { + return .failure(KeychainQueryError(status: status)) + } + return .success((privateSecKey, publicSecKey) as! R) + case .deleteAll: + status = SecItemDelete(self.raw as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + return .failure(KeychainQueryError(status: status)) + } + return .success(() as! R) + case .all: + var raw: CFTypeRef? + status = SecItemCopyMatching(self.raw as CFDictionary, &raw) + guard status == errSecSuccess, raw != nil else { + if raw == nil { + return .failure(.notFound) + } else { + return .failure(KeychainQueryError(status: status)) + } + } + return .success(raw as! R) + } + } +} diff --git a/Sources/Concrete/KeychainQueryFactory.swift b/Sources/Concrete/KeychainQueryFactory.swift new file mode 100644 index 0000000..84994ba --- /dev/null +++ b/Sources/Concrete/KeychainQueryFactory.swift @@ -0,0 +1,78 @@ +// +// KeychainQueryFactory.swift +// mew-wallet-ios-keychain +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import Foundation +import LocalAuthentication + +class KeychainQueryFactory { + static func save(_ record: KeychainRecord, accessGroup: String?) throws -> KeychainQuery { + switch record { + case let .data(data, label, account) where data != nil: + return .add(item: data!, label: label, account: account, accessGroup: accessGroup) + case let .key(key, label) where key != nil: + return .add(key: key!, label: label, accessGroup: accessGroup) + default: + throw KeychainError.general(message: "Empty data") + } + } + + static func update(_ record: KeychainRecord, accessGroup: String?) throws -> KeychainQuery { + switch record { + case let .data(data, label, account) where data != nil: + return .update(item: data!, label: label, account: account, accessGroup: accessGroup) + case let .key(key, label) where key != nil: + return .update(key: key!, label: label, accessGroup: accessGroup) + default: + throw KeychainError.general(message: "Empty data") + } + } + + static func delete(_ record: KeychainRecord, accessGroup: String?) -> KeychainQuery { + switch record { + case let .data(_, label, account): + return .delete(item: label, account: account, accessGroup: accessGroup) + case let .key(_, label): + return .delete(key: label, accessGroup: accessGroup) + } + } + + static func load(_ record: KeychainRecord, accessGroup: String?, context: LAContext?) -> KeychainQuery { + switch record { + case let .key(_, label): + return .load(key: label, accessGroup: accessGroup, context: context) + case let .data(_, label, account): + return .load(item: label, account: account, accessGroup: accessGroup) + } + } + + static func generate(_ keys: KeychainKeypair, accessGroup: String?, context: LAContext?) throws -> KeychainQuery<(prv: SecKey, pub: SecKey)> { + return try .generate(prvLabel: keys.prv.label, + pubLabel: keys.pub.label, + accessGroup: accessGroup, + context: context, + secureEnclave: keys.secureEnclave) + } + + static func deleteAll(_ record: KeychainRecord, accessGroup: String?) -> KeychainQuery { + switch record { + case .data: + return .deleteAll(keys: false, accessGroup: accessGroup) + case .key: + return .deleteAll(keys: true, accessGroup: accessGroup) + } + } + + static func all(_ record: KeychainRecord, accessGroup: String?) -> KeychainQuery<[[String: Any]]> { + switch record { + case .data: + return .all(keys: false, accessGroup: accessGroup) + case .key: + return .all(keys: true, accessGroup: accessGroup) + } + } +} diff --git a/Sources/Concrete/KeychainRecord.swift b/Sources/Concrete/KeychainRecord.swift new file mode 100644 index 0000000..2fcedf0 --- /dev/null +++ b/Sources/Concrete/KeychainRecord.swift @@ -0,0 +1,89 @@ +// +// KeychainRecord.swift +// mew-wallet-ios-keychain +// +// Created by Mikhail Nikanorov on 6/29/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import Foundation + +public struct KeychainKeypair { + public let prv: KeychainRecord + public let pub: KeychainRecord + public let secureEnclave: Bool + + public init?(prv: KeychainRecord, pub: KeychainRecord, secureEnclave: Bool = true) { + guard case .key = prv, case .key = pub else { + return nil + } + self.prv = prv + self.pub = pub + self.secureEnclave = secureEnclave + } + + public init(prv: String, pub: String, secureEnclave: Bool = true) { + self.prv = .key(key: nil, label: prv) + self.pub = .key(key: nil, label: pub) + self.secureEnclave = secureEnclave + } + + func withUpdated(keys: (prv: SecKey, pub: SecKey)) -> KeychainKeypair? { + return KeychainKeypair( + prv: .key(key: keys.prv, label: self.prv.label), + pub: .key(key: keys.pub, label: self.pub.label), + secureEnclave: self.secureEnclave + ) + } +} + +public enum KeychainRecord { + case data(data: Data?, label: String?, account: String?) + case key(key: SecKey?, label: String?) + + public var label: String? { + switch self { + case let .key(_, label): + return label + case let .data(_, label, _): + return label + } + } + + public var account: String? { + switch self { + case .key: + return nil + case let .data(_, _, account): + return account + } + } + + public var key: SecKey? { + guard case let .key(key, _) = self else { + return nil + } + return key + } + + public var data: Data? { + guard case let .data(data, _, _) = self else { + return nil + } + return data + } + + public func withUpdated(data: Data) -> KeychainRecord? { + guard case let .data(_, label, account) = self else { + return nil + } + return .data(data: data, label: label, account: account) + } + + public func withUpdated(key: SecKey) -> KeychainRecord? { + guard case let .key(_, label) = self else { + return nil + } + return .key(key: key, label: label) + } +} diff --git a/Sources/Errors/KeychainError.swift b/Sources/Errors/KeychainError.swift new file mode 100644 index 0000000..16f19bc --- /dev/null +++ b/Sources/Errors/KeychainError.swift @@ -0,0 +1,54 @@ +// +// KeychainError.swift +// mew-wallet-ios-keychain +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import Foundation +import LocalAuthentication + +public enum KeychainError: LocalizedError { + case underlying(message: String, error: NSError) + case inconcistency(message: String) + case authentication(error: LAError) + case general(message: String) + case notFound(message: String) + case invalid + + public var errorDescription: String? { + switch self { + case let .underlying(message: message, error: error): + return "\(message) \(error.localizedDescription)" + case let .authentication(error: error): + return "Authentication failed. \(error.localizedDescription)" + case let .inconcistency(message: message): + return "Inconcistency in setup, configuration or keychain. \(message)" + case let .general(message: message): + return "General error: \(message)" + case let .notFound(message): + return "Not found: \(message)" + case .invalid: + return "Invalid" + } + } + + static func fromError(_ error: CFError?, message: String) -> KeychainError { + let any = error as Any + if let authenticationError = any as? LAError { + return .authentication(error: authenticationError) + } + if let error = error, + let domain = CFErrorGetDomain(error) as String? { + let code = Int(CFErrorGetCode(error)) + var userInfo: [String: Any] = (CFErrorCopyUserInfo(error) as? [String: Any]) ?? [:] + if userInfo[NSLocalizedRecoverySuggestionErrorKey] == nil { + userInfo[NSLocalizedRecoverySuggestionErrorKey] = "See https://www.osstatus.com/search/results?platform=all&framework=all&search=\(code)" + } + let underlying = NSError(domain: domain, code: code, userInfo: userInfo) + return .underlying(message: message, error: underlying) + } + return .inconcistency(message: "\(message) Unknown error occured.") + } +} diff --git a/Sources/Extensions/LAContext+SecAccessControlCreateFlags.swift b/Sources/Extensions/LAContext+SecAccessControlCreateFlags.swift new file mode 100644 index 0000000..55d73d4 --- /dev/null +++ b/Sources/Extensions/LAContext+SecAccessControlCreateFlags.swift @@ -0,0 +1,20 @@ +// +// LAContext+SecAccessControlCreateFlags.swift +// mew-wallet-ios-keychain +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import Foundation +import LocalAuthentication + +extension LAContext { + var accessControlCreateFlags: SecAccessControlCreateFlags { + if #available(iOS 11.3, *) { + return self.isCredentialSet(.applicationPassword) ? [.applicationPassword, .privateKeyUsage] : [.biometryCurrentSet, .privateKeyUsage] + } else { + return self.isCredentialSet(.applicationPassword) ? [.applicationPassword, .privateKeyUsage] : [.touchIDCurrentSet, .privateKeyUsage] + } + } +} diff --git a/Sources/Extensions/SecAccessControlCreateFlags+SecAccessControl.swift b/Sources/Extensions/SecAccessControlCreateFlags+SecAccessControl.swift new file mode 100644 index 0000000..aa649c1 --- /dev/null +++ b/Sources/Extensions/SecAccessControlCreateFlags+SecAccessControl.swift @@ -0,0 +1,27 @@ +// +// SecAccessControlCreateFlags+SecAccessControl.swift +// mew-wallet-ios-keychain +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import Foundation + +extension SecAccessControlCreateFlags { + func createAccessControl() throws -> SecAccessControl { + if self.contains(.privateKeyUsage) { + let flagsWithOnlyPrivateKeyUsage: SecAccessControlCreateFlags = [.privateKeyUsage] + guard self != flagsWithOnlyPrivateKeyUsage else { + throw KeychainError.inconcistency(message: "Couldn't create access control with flags \(self)") + } + } + + var error: Unmanaged? + let result = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, self, &error) + guard let accessControl = result else { + throw KeychainError.fromError(error?.takeRetainedValue(), message: "Tried creating access control object with flags \(self) and protection \(kSecAttrAccessibleWhenUnlockedThisDeviceOnly)") + } + return accessControl + } +} diff --git a/Sources/Protocols/Keychain.swift b/Sources/Protocols/Keychain.swift new file mode 100644 index 0000000..3aa9506 --- /dev/null +++ b/Sources/Protocols/Keychain.swift @@ -0,0 +1,33 @@ +// +// Keychain.swift +// mew-wallet-ios-keychain +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import Foundation +import LocalAuthentication + +public protocol Keychain { + var accessGroup: String? { get } + // MARK: - Keys + func save(_ record: KeychainRecord) throws + func update(_ record: KeychainRecord) throws + func load(_ record: KeychainRecord, context: LAContext?) throws -> KeychainRecord + func delete(_ record: KeychainRecord) throws + + // MARK: - Secure Enclave + func generate(keys: KeychainKeypair, context: LAContext) throws -> KeychainKeypair + func verifySecureEnclave(context: LAContext) throws + + // MARK: - Encryption + func encrypt(pub: KeychainRecord, digest: KeychainRecord, context: LAContext) throws -> KeychainRecord + func decrypt(prv: KeychainRecord, encrypted: KeychainRecord, context: LAContext) throws -> KeychainRecord + func encryptAndSave(pub: KeychainRecord, item: KeychainRecord, context: LAContext) throws + func loadAndDecrypt(prv: KeychainRecord, item: KeychainRecord, context: LAContext) throws -> KeychainRecord + + // MARK: - Extra + func change(keys: KeychainKeypair, item: KeychainRecord, oldContext: LAContext, newContext: LAContext) throws + func reset() +} diff --git a/testApp/testApp.xcodeproj/project.pbxproj b/testApp/testApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..825b5e9 --- /dev/null +++ b/testApp/testApp.xcodeproj/project.pbxproj @@ -0,0 +1,505 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 5672739B268AAC34002CC09B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5672739A268AAC34002CC09B /* AppDelegate.swift */; }; + 5672739D268AAC34002CC09B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5672739C268AAC34002CC09B /* SceneDelegate.swift */; }; + 5672739F268AAC34002CC09B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5672739E268AAC34002CC09B /* ViewController.swift */; }; + 567273A2268AAC34002CC09B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 567273A0268AAC34002CC09B /* Main.storyboard */; }; + 567273A4268AAC35002CC09B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 567273A3268AAC35002CC09B /* Assets.xcassets */; }; + 567273A7268AAC35002CC09B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 567273A5268AAC35002CC09B /* LaunchScreen.storyboard */; }; + 567273B2268AAC35002CC09B /* testAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567273B1268AAC35002CC09B /* testAppTests.swift */; }; + 569E19EF268D3A64006B0AC9 /* mew-wallet-ios-keychain in Frameworks */ = {isa = PBXBuildFile; productRef = 569E19EE268D3A64006B0AC9 /* mew-wallet-ios-keychain */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 567273AE268AAC35002CC09B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5672738F268AAC34002CC09B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 56727396268AAC34002CC09B; + remoteInfo = testApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 56727397268AAC34002CC09B /* testApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = testApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5672739A268AAC34002CC09B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 5672739C268AAC34002CC09B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 5672739E268AAC34002CC09B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 567273A1268AAC34002CC09B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 567273A3268AAC35002CC09B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 567273A6268AAC35002CC09B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 567273A8268AAC35002CC09B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 567273AD268AAC35002CC09B /* testAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = testAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 567273B1268AAC35002CC09B /* testAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = testAppTests.swift; sourceTree = ""; }; + 567273B3268AAC35002CC09B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 567273CA268AB00F002CC09B /* testApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = testApp.entitlements; sourceTree = ""; }; + 569E19ED268D3A57006B0AC9 /* mew-wallet-ios-keychain */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "mew-wallet-ios-keychain"; path = ..; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 56727394268AAC34002CC09B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 569E19EF268D3A64006B0AC9 /* mew-wallet-ios-keychain in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 567273AA268AAC35002CC09B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5672738E268AAC34002CC09B = { + isa = PBXGroup; + children = ( + 569E19ED268D3A57006B0AC9 /* mew-wallet-ios-keychain */, + 56727399268AAC34002CC09B /* testApp */, + 567273B0268AAC35002CC09B /* testAppTests */, + 56727398268AAC34002CC09B /* Products */, + 567273D0268AB236002CC09B /* Frameworks */, + ); + sourceTree = ""; + }; + 56727398268AAC34002CC09B /* Products */ = { + isa = PBXGroup; + children = ( + 56727397268AAC34002CC09B /* testApp.app */, + 567273AD268AAC35002CC09B /* testAppTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 56727399268AAC34002CC09B /* testApp */ = { + isa = PBXGroup; + children = ( + 567273CA268AB00F002CC09B /* testApp.entitlements */, + 5672739A268AAC34002CC09B /* AppDelegate.swift */, + 5672739C268AAC34002CC09B /* SceneDelegate.swift */, + 5672739E268AAC34002CC09B /* ViewController.swift */, + 567273A0268AAC34002CC09B /* Main.storyboard */, + 567273A3268AAC35002CC09B /* Assets.xcassets */, + 567273A5268AAC35002CC09B /* LaunchScreen.storyboard */, + 567273A8268AAC35002CC09B /* Info.plist */, + ); + path = testApp; + sourceTree = ""; + }; + 567273B0268AAC35002CC09B /* testAppTests */ = { + isa = PBXGroup; + children = ( + 567273B1268AAC35002CC09B /* testAppTests.swift */, + 567273B3268AAC35002CC09B /* Info.plist */, + ); + path = testAppTests; + sourceTree = ""; + }; + 567273D0268AB236002CC09B /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 56727396268AAC34002CC09B /* testApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 567273C1268AAC35002CC09B /* Build configuration list for PBXNativeTarget "testApp" */; + buildPhases = ( + 56727393268AAC34002CC09B /* Sources */, + 56727394268AAC34002CC09B /* Frameworks */, + 56727395268AAC34002CC09B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = testApp; + packageProductDependencies = ( + 569E19EE268D3A64006B0AC9 /* mew-wallet-ios-keychain */, + ); + productName = testApp; + productReference = 56727397268AAC34002CC09B /* testApp.app */; + productType = "com.apple.product-type.application"; + }; + 567273AC268AAC35002CC09B /* testAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 567273C4268AAC35002CC09B /* Build configuration list for PBXNativeTarget "testAppTests" */; + buildPhases = ( + 567273A9268AAC35002CC09B /* Sources */, + 567273AA268AAC35002CC09B /* Frameworks */, + 567273AB268AAC35002CC09B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 567273AF268AAC35002CC09B /* PBXTargetDependency */, + ); + name = testAppTests; + productName = testAppTests; + productReference = 567273AD268AAC35002CC09B /* testAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5672738F268AAC34002CC09B /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1250; + LastUpgradeCheck = 1250; + TargetAttributes = { + 56727396268AAC34002CC09B = { + CreatedOnToolsVersion = 12.5.1; + }; + 567273AC268AAC35002CC09B = { + CreatedOnToolsVersion = 12.5.1; + TestTargetID = 56727396268AAC34002CC09B; + }; + }; + }; + buildConfigurationList = 56727392268AAC34002CC09B /* Build configuration list for PBXProject "testApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5672738E268AAC34002CC09B; + packageReferences = ( + ); + productRefGroup = 56727398268AAC34002CC09B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 56727396268AAC34002CC09B /* testApp */, + 567273AC268AAC35002CC09B /* testAppTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 56727395268AAC34002CC09B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 567273A7268AAC35002CC09B /* LaunchScreen.storyboard in Resources */, + 567273A4268AAC35002CC09B /* Assets.xcassets in Resources */, + 567273A2268AAC34002CC09B /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 567273AB268AAC35002CC09B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 56727393268AAC34002CC09B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5672739F268AAC34002CC09B /* ViewController.swift in Sources */, + 5672739B268AAC34002CC09B /* AppDelegate.swift in Sources */, + 5672739D268AAC34002CC09B /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 567273A9268AAC35002CC09B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 567273B2268AAC35002CC09B /* testAppTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 567273AF268AAC35002CC09B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 56727396268AAC34002CC09B /* testApp */; + targetProxy = 567273AE268AAC35002CC09B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 567273A0268AAC34002CC09B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 567273A1268AAC34002CC09B /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 567273A5268AAC35002CC09B /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 567273A6268AAC35002CC09B /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 567273BF268AAC35002CC09B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 567273C0268AAC35002CC09B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 567273C2268AAC35002CC09B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = testApp/testApp.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = D4VM3QEZ9V; + INFOPLIST_FILE = testApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = test.organization.testApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 567273C3268AAC35002CC09B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = testApp/testApp.entitlements; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = D4VM3QEZ9V; + INFOPLIST_FILE = testApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = test.organization.testApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 567273C5268AAC35002CC09B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = D4VM3QEZ9V; + INFOPLIST_FILE = testAppTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = test.organization.testAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/testApp.app/testApp"; + }; + name = Debug; + }; + 567273C6268AAC35002CC09B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = D4VM3QEZ9V; + INFOPLIST_FILE = testAppTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = test.organization.testAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/testApp.app/testApp"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 56727392268AAC34002CC09B /* Build configuration list for PBXProject "testApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 567273BF268AAC35002CC09B /* Debug */, + 567273C0268AAC35002CC09B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 567273C1268AAC35002CC09B /* Build configuration list for PBXNativeTarget "testApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 567273C2268AAC35002CC09B /* Debug */, + 567273C3268AAC35002CC09B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 567273C4268AAC35002CC09B /* Build configuration list for PBXNativeTarget "testAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 567273C5268AAC35002CC09B /* Debug */, + 567273C6268AAC35002CC09B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 569E19EE268D3A64006B0AC9 /* mew-wallet-ios-keychain */ = { + isa = XCSwiftPackageProductDependency; + productName = "mew-wallet-ios-keychain"; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 5672738F268AAC34002CC09B /* Project object */; +} diff --git a/testApp/testApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/testApp/testApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/testApp/testApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/testApp/testApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/testApp/testApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/testApp/testApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/testApp/testApp.xcodeproj/xcshareddata/xcschemes/testApp.xcscheme b/testApp/testApp.xcodeproj/xcshareddata/xcschemes/testApp.xcscheme new file mode 100644 index 0000000..2d03cc8 --- /dev/null +++ b/testApp/testApp.xcodeproj/xcshareddata/xcschemes/testApp.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testApp/testApp/AppDelegate.swift b/testApp/testApp/AppDelegate.swift new file mode 100644 index 0000000..f2d54f1 --- /dev/null +++ b/testApp/testApp/AppDelegate.swift @@ -0,0 +1,37 @@ +// +// AppDelegate.swift +// testApp +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/testApp/testApp/Assets.xcassets/AccentColor.colorset/Contents.json b/testApp/testApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/testApp/testApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/testApp/testApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/testApp/testApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/testApp/testApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/testApp/testApp/Assets.xcassets/Contents.json b/testApp/testApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/testApp/testApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/testApp/testApp/Base.lproj/LaunchScreen.storyboard b/testApp/testApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/testApp/testApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testApp/testApp/Base.lproj/Main.storyboard b/testApp/testApp/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/testApp/testApp/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testApp/testApp/Info.plist b/testApp/testApp/Info.plist new file mode 100644 index 0000000..5b531f7 --- /dev/null +++ b/testApp/testApp/Info.plist @@ -0,0 +1,66 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/testApp/testApp/SceneDelegate.swift b/testApp/testApp/SceneDelegate.swift new file mode 100644 index 0000000..f4baadc --- /dev/null +++ b/testApp/testApp/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// testApp +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/testApp/testApp/ViewController.swift b/testApp/testApp/ViewController.swift new file mode 100644 index 0000000..5fc5fc8 --- /dev/null +++ b/testApp/testApp/ViewController.swift @@ -0,0 +1,20 @@ +// +// ViewController.swift +// testApp +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/testApp/testApp/testApp.entitlements b/testApp/testApp/testApp.entitlements new file mode 100644 index 0000000..ed6d897 --- /dev/null +++ b/testApp/testApp/testApp.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.keychain.testapp + + keychain-access-groups + + $(AppIdentifierPrefix)group.keychain.testapp + + + diff --git a/testApp/testAppTests/Info.plist b/testApp/testAppTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/testApp/testAppTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/testApp/testAppTests/testAppTests.swift b/testApp/testAppTests/testAppTests.swift new file mode 100644 index 0000000..88f2e29 --- /dev/null +++ b/testApp/testAppTests/testAppTests.swift @@ -0,0 +1,476 @@ +// +// testAppTests.swift +// testAppTests +// +// Created by Mikhail Nikanorov on 6/28/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import XCTest +import LocalAuthentication +@testable import testApp +@testable import mew_wallet_ios_keychain + +class testAppTests: XCTestCase { + static var keychain: Keychain! + static var badkeychain: Keychain! + static var key: SecKey! + + override class func setUp() { + keychain = KeychainImplementation(accessGroup: "group.keychain.testapp") + badkeychain = KeychainImplementation(accessGroup: "group22.keychain22.testapp22") + + let keyBase64 = "MIIEpAIBAAKCAQEA5B7lqLrwVCFNUiCmwMr5Q48iuArOolxb7DAuclGnoZVX0SaJ8mrvCOtd6qY/VeBw227txWEPH7840qX/yGxxqTngdNCuDATqYrrbxFOGV30GZmg6NpZYKShTlsftkhiCsoXW0A7m5MCZUkH2/sNBC8oRHCNDXRlsU5bq/yPaAMt6xlBsUgLt/++INcuw+rx1Rm7LJv0FeukQmlekUOL/DMJXcLXCa05StTbvHPiAHOLej07pThCZoX3XHFpOTQ6379EsjvSZHtNhr67qrtRb8or2rX7wt5NWzXHbhUDlyzEcIBB/7G8ygqWhyZTEIMFiRMWSa3KGYZE3nZe5weC7SQIDAQABAoIBAQCjjxehA+++kmYK5YhKIP3Zl64QAQeo18m8rcsPgkZLj3V4a0Zq/orGfWNIE8zDePnSC1YFuBKM86D9P7IGdOKFsA6kEt9HlNqs0UczG6Pt5KGLGV3rt54cXGKacFyA7HwBHf8oDBc2mnUTymIaxcpEdqwP3aS2Ar1trX5uUrlC6UcZyspBZVYvMlU+uAKL1ZtFxjsv0EzuQQW1HX7b2WPUAoxp/yBC/EBRM9K8WbG9i7NB4FTFHAdTMt/EZLGUESizFgrai6lp3s96Apz5GvncRUI+UVP/7zbUaFYdRMW5lrcR8+PL9NACkL2rnQuLoyLKWZWPPlD3WEE9EzY4bH6FAoGBAPu8hL8goEbWMFDZuox04Ouy6EpXR8BDTq8ut6hmad6wpFgZD15Xu7pYEbbsPntdYODKDDAIJCBsiBgf2emL50BpiQkzMPhxyMsN5Pzry9Ys+AzPkJcQ7g+/Wbto9lCC+JmgxtGQ7JIibo1QH7BTsuK9+k72HnZne6oIfaYKbBZfAoGBAOf7/D2Q7NiNcEgxpZRn06+mnkHMb8PfCKfJf/BFf5WKXSkDBZ3XhWSPnZyQnE3gW3lzJjzUwHS+YDk8A0Xl2piAHa/d5O/8eoijB8wa6UGVDBIXqUnfM3Udfry78rM71FOpbzV3H48G7u4CUJMGwOpEqF0TfgtQr4uf8OurdH9XAoGAbdNhVsE1K7Jmgd97s6uKNUpobYaGlyrGOUd4eM+1gKIwEP9d5RsBm9qwX83RtKCYk3mSt6HVoQ+4kE3VFD8lNMTWNF1REBMUNwJo1K9KzrXvwicMPdv1AInK7ChuzdFWBDBQjT1c+KRs9tnt+U+Ky8F2Ytydjaq4GQZ7SuVhIqECgYBMsS+IovrJ9KhkFZWp5FFFRo4XLqDcXkWcQq87HZ66L03xGwCmV/PPdPMkKWKjFELpebnwbl1Zuv5QrZhfaUfFFsW5uF/RPuS7ezo+rb7jYYTmDlB3DYUTeLbHalMoEeV16xPK1yDlxeMDaFx+3sK0MBKBAsqurvP58txQ7RPMbQKBgQCzRcURopG0DF4VF4+xQJS8FpTcnQsQnO/2MJR35npA2iUb+ffs+0lgEdeWs4W46kvaF1iVEPbr6She+aKROzE9Bs25ZCgGLv97oUxDQo0IPvURX7ucN+xOUU1hw9oQDVdGKl1JZh93fn+bjtMTe+26asGLmM0r9YQX1P8qaw3KOg==" + + let keyData = Data(base64Encoded: keyBase64)! + key = SecKeyCreateWithData(keyData as NSData, [ + kSecAttrKeyType: kSecAttrKeyTypeRSA, + kSecAttrKeyClass: kSecAttrKeyClassPrivate, + ] as NSDictionary, nil)! + } + + override func setUp() { + try? testAppTests.keychain.delete(.key(key: nil, label: "key")) + try? testAppTests.keychain.delete(.key(key: nil, label: "pub")) + try? testAppTests.keychain.delete(.key(key: nil, label: "key-change")) + try? testAppTests.keychain.delete(.key(key: nil, label: "pub-change")) + try? testAppTests.keychain.delete(.data(data: nil, label: "message", account: nil)) + try? testAppTests.keychain.delete(.data(data: nil, label: "message-change", account: nil)) + } + + override func tearDown() { + try? testAppTests.keychain.delete(.key(key: nil, label: "key")) + try? testAppTests.keychain.delete(.key(key: nil, label: "pub")) + try? testAppTests.keychain.delete(.key(key: nil, label: "key-change")) + try? testAppTests.keychain.delete(.key(key: nil, label: "pub-change")) + try? testAppTests.keychain.delete(.data(data: nil, label: "message", account: nil)) + try? testAppTests.keychain.delete(.data(data: nil, label: "message-change", account: nil)) + } + + func testBadKeychainShouldThrowError() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + + XCTAssertThrowsError(try testAppTests.badkeychain.save(.data(data: Data(), label: "label", account: nil))) + XCTAssertThrowsError(try testAppTests.badkeychain.update(.data(data: Data(), label: "label", account: nil))) + XCTAssertThrowsError(try testAppTests.badkeychain.load(.data(data: Data(), label: "label", account: nil), context: nil)) + XCTAssertThrowsError(try testAppTests.badkeychain.delete(.data(data: Data(), label: "label", account: nil))) + XCTAssertThrowsError(try testAppTests.badkeychain.generate(keys: KeychainKeypair(prv: "prv", pub: "pub"), context: laContext)) + XCTAssertThrowsError(try testAppTests.badkeychain.verifySecureEnclave(context: laContext)) + XCTAssertThrowsError(try testAppTests.badkeychain.encrypt(pub: .key(key: nil, label: "pub"), digest: .data(data: Data(), label: nil, account: nil), context: laContext)) + XCTAssertThrowsError(try testAppTests.badkeychain.decrypt(prv: .key(key: nil, label: "prv"), encrypted: .data(data: Data(), label: nil, account: nil), context: laContext)) + XCTAssertThrowsError(try testAppTests.badkeychain.encryptAndSave(pub: .key(key: nil, label: "pub"), item: .data(data: Data(), label: nil, account: nil), context: laContext)) + XCTAssertThrowsError(try testAppTests.badkeychain.loadAndDecrypt(prv: .key(key: nil, label: "prv"), item: .data(data: Data(), label: nil, account: nil), context: laContext)) + XCTAssertThrowsError(try testAppTests.badkeychain.change(keys: KeychainKeypair(prv: "prv", pub: "pub"), item: .data(data: Data(), label: nil, account: nil), oldContext: laContext, newContext: laContext)) + } + + func testSaveKey() { + do { + try testAppTests.keychain.save(.key(key: testAppTests.key, label: "key")) + let key = try testAppTests.keychain.load(.key(key: nil, label: "key"), context: nil).key + XCTAssertEqual(testAppTests.key, key) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testDoubleSaveKey() { + XCTAssertNoThrow(try testAppTests.keychain.save(.key(key: testAppTests.key, label: "key"))) + XCTAssertThrowsError(try testAppTests.keychain.save(.key(key: testAppTests.key, label: "key"))) + } + + func testUpdateShouldSave() { + do { + let text = "This is test data" + let data = text.data(using: .utf8)! + + try testAppTests.keychain.update(.data(data: data, label: "message", account: nil)) + let loaded = try testAppTests.keychain.load(.data(data: nil, label: "message", account: nil), context: nil) + let loadedText = String(data: loaded.data!, encoding: .utf8) + + XCTAssertEqual(data, loaded.data) + XCTAssertEqual(text, loadedText) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUpdateShouldUpdate() { + do { + let text = "This is test data" + let updated = "This is updated data" + let data = text.data(using: .utf8)! + let updatedData = updated.data(using: .utf8)! + + try testAppTests.keychain.update(.data(data: data, label: "message", account: nil)) + let loaded = try testAppTests.keychain.load(.data(data: nil, label: "message", account: nil), context: nil) + let loadedText = String(data: loaded.data!, encoding: .utf8) + + XCTAssertEqual(data, loaded.data) + XCTAssertEqual(text, loadedText) + + try testAppTests.keychain.update(.data(data: updatedData, label: "message", account: nil)) + let loadedUpdate = try testAppTests.keychain.load(.data(data: nil, label: "message", account: nil), context: nil) + let loadedTextUpdate = String(data: loadedUpdate.data!, encoding: .utf8) + + XCTAssertEqual(updatedData, loadedUpdate.data) + XCTAssertEqual(updated, loadedTextUpdate) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUpdateShouldNotUpdateNorSaveKey() { + XCTAssertThrowsError(try testAppTests.keychain.update(.key(key: testAppTests.key, label: "key"))) + } + + func testReset() { + do { + try testAppTests.keychain.save(.key(key: testAppTests.key, label: "key")) + XCTAssertNoThrow(try testAppTests.keychain.load(.key(key: nil, label: "key"), context: nil)) + + let text = "This is test data" + let data = text.data(using: .utf8)! + + try testAppTests.keychain.update(.data(data: data, label: "message", account: nil)) + let loaded = try testAppTests.keychain.load(.data(data: nil, label: "message", account: nil), context: nil) + let loadedText = String(data: loaded.data!, encoding: .utf8) + + XCTAssertEqual(data, loaded.data) + XCTAssertEqual(text, loadedText) + + testAppTests.keychain.reset() + + XCTAssertThrowsError(try testAppTests.keychain.load(.key(key: nil, label: "key"), context: nil)) + XCTAssertThrowsError(try testAppTests.keychain.load(.data(data: nil, label: "message", account: nil), context: nil)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testDeleteKey() { + do { + try testAppTests.keychain.save(.key(key: testAppTests.key, label: "key")) + XCTAssertNoThrow(try testAppTests.keychain.load(.key(key: nil, label: "key"), context: nil)) + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + XCTAssertThrowsError(try testAppTests.keychain.load(.key(key: nil, label: "key"), context: nil)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGenerateKey() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + do { + let keys = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + XCTAssertNotNil(keys.prv) + XCTAssertNotNil(keys.pub) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGenerateAndSaveKey() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + do { + let keys = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + try testAppTests.keychain.delete(.key(key: nil, label: "pub")) + try testAppTests.keychain.save(keys.pub) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGenerateAndDoubleSaveKey() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + do { + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + let keys = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + let load = try testAppTests.keychain.load(.key(key: nil, label: "key"), context: nil).key + XCTAssertThrowsError(try testAppTests.keychain.save(keys.pub)) + XCTAssertEqual(load, keys.prv.key) + XCTAssertNotNil(load) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testGenerateEncryptAndDecrypt() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + do { + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + try testAppTests.keychain.delete(.key(key: nil, label: "pub")) + + laContext.setCredential(credential, type: .applicationPassword) + _ = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + + let text = "This is test data" + let data = text.data(using: .utf8)! + let encrypted = try testAppTests.keychain.encrypt(pub: .key(key: nil, label: "pub"), + digest: .data(data: data, label: nil, account: nil), + context: laContext) + + let decrypted = try testAppTests.keychain.decrypt(prv: .key(key: nil, label: "key"), + encrypted: encrypted, + context: laContext) + guard let decryptedData = decrypted.data else { + XCTFail("Empty data") + return + } + let decryptedText = String(data: decryptedData, encoding: .utf8) + + XCTAssertEqual(data, decryptedData) + XCTAssertEqual(decryptedText, text) + + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + try testAppTests.keychain.delete(.key(key: nil, label: "pub")) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testDoubleGenerate() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + XCTAssertNoThrow(try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext)) + XCTAssertThrowsError(try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext)) + + do { + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testValidateSecureEnclave() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + XCTAssertNoThrow(try testAppTests.keychain.verifySecureEnclave(context: laContext)) + } + + func testSaveItem() { + do { + let text = "This is test message" + let data = text.data(using: .utf8)! + try testAppTests.keychain.delete(.data(data: nil, label: "message", account: nil)) + defer { + try? testAppTests.keychain.delete(.data(data: nil, label: "message", account: nil)) + } + + XCTAssertNoThrow(try testAppTests.keychain.save(.data(data: data, label: "message", account: nil))) + XCTAssertThrowsError(try testAppTests.keychain.save(.data(data: data, label: "message", account: nil))) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testSaveAndLoadItem() { + do { + let text = "This is test message" + let data = text.data(using: .utf8)! + try testAppTests.keychain.delete(.data(data: nil, label: "message", account: nil)) + defer { + try? testAppTests.keychain.delete(.data(data: nil, label: "message", account: nil)) + } + try testAppTests.keychain.save(.data(data: data, label: "message", account: nil)) + + guard let loaded = try testAppTests.keychain.load(.data(data: nil, label: "message", account: nil), context: nil).data else { + XCTFail("item not found") + return + } + let loadedText = String(data: loaded, encoding: .utf8) + XCTAssertEqual(data, loaded) + XCTAssertEqual(text, loadedText) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testEncryptAndSaveItem() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + + do { + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + try testAppTests.keychain.delete(.key(key: nil, label: "pub")) + + laContext.setCredential(credential, type: .applicationPassword) + _ = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + + let text = "This is test message" + let data = text.data(using: .utf8)! + + try testAppTests.keychain.encryptAndSave(pub: .key(key: nil, label: "pub"), + item: .data(data: data, label: "message", account: nil), + context: laContext) + + let encrypted = try testAppTests.keychain.load(.data(data: nil, label: "message", account: nil), context: nil).data + + XCTAssertNotEqual(data, encrypted) + + let decrypted = try testAppTests.keychain.loadAndDecrypt(prv: .key(key: nil, label: "key"), + item: .data(data: nil, label: "message", account: nil), + context: laContext) + guard let decryptedData = decrypted.data else { + XCTFail("Empty data") + return + } + let decryptedText = String(data: decryptedData, encoding: .utf8) + + XCTAssertEqual(data, decryptedData) + XCTAssertEqual(text, decryptedText) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testEncryptAndSaveItemAndRegenerateKeys() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + + do { + + laContext.setCredential(credential, type: .applicationPassword) + _ = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + + let text = "This is test message" + let data = text.data(using: .utf8)! + + try testAppTests.keychain.encryptAndSave(pub: .key(key: nil, label: "pub"), + item: .data(data: data, label: "message", account: nil), + context: laContext) + + let encrypted = try testAppTests.keychain.load(.data(data: nil, label: "message", account: nil), context: nil).data + + XCTAssertNotEqual(data, encrypted) + + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + try testAppTests.keychain.delete(.key(key: nil, label: "pub")) + + _ = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + + XCTAssertThrowsError(try testAppTests.keychain.loadAndDecrypt(prv: .key(key: nil, label: "key"), + item: .data(data: nil, label: "message", + account: nil), + context: laContext)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testEncryptAndSaveItemAndReplaceKeys() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + + let newCredential = Data([0x01, 0x01, 0x01, 0x01, 0x01, 0x01]) + let newLaContext = LAContext() + newLaContext.setCredential(newCredential, type: .applicationPassword) + + do { + _ = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + + let text = "This is test message" + let data = text.data(using: .utf8)! + + try testAppTests.keychain.encryptAndSave(pub: .key(key: nil, label: "pub"), + item: .data(data: data, label: "message", account: nil), + context: laContext) + + try testAppTests.keychain.change(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), + item: .data(data: nil, label: "message", account: nil), + oldContext: laContext, + newContext: newLaContext) + + let decrypted = try testAppTests.keychain.loadAndDecrypt(prv: .key(key: nil, label: "key"), + item: .data(data: nil, label: "message", account: nil), + context: newLaContext) + guard let decryptedData = decrypted.data else { + XCTFail("Empty data") + return + } + let decryptedText = String(data: decryptedData, encoding: .utf8) + + XCTAssertEqual(data, decryptedData) + XCTAssertEqual(text, decryptedText) + + XCTAssertThrowsError(try testAppTests.keychain.loadAndDecrypt(prv: .key(key: nil, label: "key"), + item: .data(data: nil, label: "message", account: nil), + context: laContext)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testResave() { + let credential = Data([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let laContext = LAContext() + laContext.setCredential(credential, type: .applicationPassword) + + let newCredential = Data([0x01, 0x01, 0x01, 0x01, 0x01, 0x01]) + let newLaContext = LAContext() + newLaContext.setCredential(newCredential, type: .applicationPassword) + + do { + _ = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: laContext) + + let text = "This is test message" + let data = text.data(using: .utf8)! + + try testAppTests.keychain.encryptAndSave(pub: .key(key: nil, label: "pub"), + item: .data(data: data, label: "message", account: nil), + context: laContext) + + let decrypted = try testAppTests.keychain.loadAndDecrypt(prv: .key(key: nil, label: "key"), + item: .data(data: data, label: "message", account: nil), + context: laContext) + + try testAppTests.keychain.delete(.key(key: nil, label: "key")) + try testAppTests.keychain.delete(.key(key: nil, label: "pub")) + try testAppTests.keychain.delete(.data(data: nil, label: "message", account: nil)) + + _ = try testAppTests.keychain.generate(keys: KeychainKeypair(prv: "key", pub: "pub", secureEnclave: true), context: newLaContext) + + try testAppTests.keychain.encryptAndSave(pub: .key(key: nil, label: "pub"), + item: .data(data: data, label: "message", account: nil), + context: newLaContext) + + let decrypted2 = try testAppTests.keychain.loadAndDecrypt(prv: .key(key: nil, label: "key"), + item: .data(data: nil, label: "message", account: nil), + context: newLaContext) + + guard let decryptedData = decrypted.data, let decryptedData2 = decrypted2.data else { + XCTFail("Empty data") + return + } + + let decryptedText = String(data: decryptedData, encoding: .utf8) + let decryptedText2 = String(data: decryptedData2, encoding: .utf8) + + XCTAssertEqual(data, decryptedData) + XCTAssertEqual(text, decryptedText) + + XCTAssertEqual(decryptedData, decryptedData2) + XCTAssertEqual(decryptedText, decryptedText2) + + XCTAssertThrowsError(try testAppTests.keychain.loadAndDecrypt(prv: .key(key: nil, label: "key"), + item: .data(data: nil, label: "message", account: nil), + context: laContext)) + } catch { + XCTFail(error.localizedDescription) + } + } +}