diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-05-11 18:27:44 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-05-11 18:27:44 +0200 |
| commit | 5151488fcd871f84a8d6955225d5da5d0155e347 (patch) | |
| tree | b9b2579c73f2109e6077799c70966f0d76540682 | |
| parent | 70e1e7e48b8a49a7e227ca538dfa56a309791344 (diff) | |
| parent | 5a029132f5f640f39a1fb908732bb3654d23c936 (diff) | |
| download | mullvadvpn-5151488fcd871f84a8d6955225d5da5d0155e347.tar.xz mullvadvpn-5151488fcd871f84a8d6955225d5da5d0155e347.zip | |
Merge branch 'add-keychain-abstractions'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 32 | ||||
| -rw-r--r-- | ios/MullvadVPN/Keychain.swift | 154 | ||||
| -rw-r--r-- | ios/MullvadVPN/KeychainAttributes.swift | 139 | ||||
| -rw-r--r-- | ios/MullvadVPN/KeychainClass.swift | 50 | ||||
| -rw-r--r-- | ios/MullvadVPN/KeychainError.swift | 22 | ||||
| -rw-r--r-- | ios/MullvadVPN/KeychainMatchLimit.swift | 48 | ||||
| -rw-r--r-- | ios/MullvadVPN/KeychainReturn.swift | 57 |
7 files changed, 493 insertions, 9 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 6838aa16d4..047c0f3a1e 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -130,6 +130,17 @@ 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; }; 58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */; }; 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; }; + 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; + 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; + 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; + 58FAEDF7245088E100CB0F5B /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; + 58FAEDF8245088E100CB0F5B /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; + 58FAEDFD24533A5500CB0F5B /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; + 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; }; + 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; + 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; }; + 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; }; + 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */; }; 58FD5BE92419406000112C88 /* SKRequestPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE82419406000112C88 /* SKRequestPublisher.swift */; }; 58FD5BEC2420F58A00112C88 /* SKPaymentQueuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEB2420F58A00112C88 /* SKPaymentQueuePublisher.swift */; }; @@ -277,6 +288,11 @@ 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+MullvadVersion.swift"; sourceTree = "<group>"; }; 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; }; 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerActivityIndicatorView.swift; sourceTree = "<group>"; }; + 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAttributes.swift; sourceTree = "<group>"; }; + 58FAEDF6245088E100CB0F5B /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; }; + 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainMatchLimit.swift; sourceTree = "<group>"; }; + 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainReturn.swift; sourceTree = "<group>"; }; + 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainClass.swift; sourceTree = "<group>"; }; 58FBDAA422A52BDA00EB69A3 /* PacketTunnel-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PacketTunnel-Bridging-Header.h"; sourceTree = "<group>"; }; 58FBDAAA22A52DC500EB69A3 /* MullvadVPN-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MullvadVPN-Bridging-Header.h"; sourceTree = "<group>"; }; 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReceipt.swift; sourceTree = "<group>"; }; @@ -400,7 +416,12 @@ 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */, 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */, 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */, + 58FAEDF6245088E100CB0F5B /* Keychain.swift */, + 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */, + 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */, 58AEEF642344A36000C9BBD5 /* KeychainError.swift */, + 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */, + 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */, 58CE5E6C224146210008646E /* LaunchScreen.storyboard */, 58A1AA8623F43901009F7EA6 /* Location.swift */, 58BA692D23E99EFF009DC256 /* Locking.swift */, @@ -739,6 +760,7 @@ 58B0A2AC238EE6D500BC001D /* IpAddress+Codable.swift in Sources */, 58B0A2AB238EE6BF00BC001D /* RelayList.swift in Sources */, 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */, + 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */, 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */, @@ -764,17 +786,21 @@ 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */, 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */, 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */, + 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */, 58C6B35422BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */, 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */, + 58FAEDF7245088E100CB0F5B /* Keychain.swift in Sources */, 5888AD87227B17950051EB06 /* SelectLocationController.swift in Sources */, 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, 584E96BA240D791E00D3334F /* CancellableDelayPublisher.swift in Sources */, 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */, + 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */, 5845F838236C466400B2D93C /* TunnelControlViewController.swift in Sources */, 58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */, 58AEEF6B2344A46200C9BBD5 /* TunnelConfigurationManager.swift in Sources */, 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */, 581CBCEC2298041B00727D7F /* SettingsAppVersionCell.swift in Sources */, + 58FAEDFD24533A5500CB0F5B /* KeychainMatchLimit.swift in Sources */, 5845F83A236C6A7200B2D93C /* AutoDisposableSink.swift in Sources */, 5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 58FD5BEC2420F58A00112C88 /* SKPaymentQueuePublisher.swift in Sources */, @@ -823,6 +849,7 @@ 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */, 58AEEF682344A40800C9BBD5 /* TunnelConfigurationCoder.swift in Sources */, 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, + 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -832,6 +859,8 @@ files = ( 5845F83C236C72E300B2D93C /* AutoDisposableSink.swift in Sources */, 5860F1C423A8D25F00CEA666 /* WireguardConfiguration.swift in Sources */, + 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */, + 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */, 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 58BFA5C222A7C92900A6173D /* JsonRpc.swift in Sources */, 588AE730236200E2009F9F2E /* MutuallyExclusive.swift in Sources */, @@ -844,12 +873,14 @@ 58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */, 58C6B36522C10596003C19AD /* AnyIPEndpoint+Wireguard.swift in Sources */, 58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */, + 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */, 586AA296234B696B00502875 /* WireguardAssociatedAddresses.swift in Sources */, 58BA692F23E99F5B009DC256 /* Locking.swift in Sources */, 58B8743B22B788D200015324 /* PacketTunnelSettingsGenerator.swift in Sources */, 5860F1EB23AA4CF300CEA666 /* Logging.swift in Sources */, 5860F1C223A785C600CEA666 /* WireguardDevice.swift in Sources */, 58C6B35522BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */, + 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */, 58AEEF6C2344A49D00C9BBD5 /* TunnelConfigurationManager.swift in Sources */, 58C6B35F22BBBFE3003C19AD /* Data+HexCoding.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, @@ -859,6 +890,7 @@ 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, 58AEEF692344A43A00C9BBD5 /* TunnelConfigurationCoder.swift in Sources */, 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, + 58FAEDF8245088E100CB0F5B /* Keychain.swift in Sources */, 58C6B36122C0EC82003C19AD /* AnyIPEndpoint+DNS64.swift in Sources */, 58C6B36722C106FC003C19AD /* WireguardCommand.swift in Sources */, 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, diff --git a/ios/MullvadVPN/Keychain.swift b/ios/MullvadVPN/Keychain.swift new file mode 100644 index 0000000000..383219990a --- /dev/null +++ b/ios/MullvadVPN/Keychain.swift @@ -0,0 +1,154 @@ +// +// Keychain.swift +// MullvadVPN +// +// Created by pronebird on 22/04/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Security + +protocol KeychainAttributeDecodable { + init?(attributes: [CFString: Any]) +} + +protocol KeychainAttributeEncodable { + func keychainRepresentation() -> [CFString: Any] + func updateKeychainAttributes(in attributes: inout [CFString: Any]) +} + +extension KeychainAttributeEncodable { + func keychainRepresentation() -> [CFString: Any] { + var attributes = [CFString: Any]() + updateKeychainAttributes(in: &attributes) + return attributes + } +} + +enum Keychain {} + +extension Keychain { + + /// A Keychain Result type + typealias Result<T> = Swift.Result<T, Keychain.Error> + + static func add(_ attributes: Keychain.Attributes) -> Result<Keychain.Attributes?> { + var result: CFTypeRef? + let status = SecItemAdd(attributes.keychainRepresentation() as CFDictionary, &result) + + return mapSecResultAndReturnValue( + status: status, + value: result, + returnSet: attributes.return ?? [], + limit: .one) + .map { $0.first } + } + + static func update(query: Keychain.Attributes, update: Keychain.Attributes) -> Result<()> { + let queryAttributes = query.keychainRepresentation() as CFDictionary + let updateAttributes = update.keychainRepresentation() as CFDictionary + + let status = SecItemUpdate(queryAttributes, updateAttributes) + + return mapSecResult(status: status) { + return () + } + } + + static func delete(query: Keychain.Attributes) -> Result<()> { + let status = SecItemDelete(query.keychainRepresentation() as CFDictionary) + + return mapSecResult(status: status) { + return () + } + } + + static func findFirst(query: Keychain.Attributes) -> Result<Keychain.Attributes?> { + return find(query: query).map { $0.first } + } + + static func find(query: Keychain.Attributes) -> Result<[Keychain.Attributes]> { + let attributes = query.keychainRepresentation() + + var result: CFTypeRef? + let status = SecItemCopyMatching(attributes as CFDictionary, &result) + + return mapSecResultAndReturnValue( + status: status, + value: result, + returnSet: query.return ?? [], + limit: query.matchLimit ?? .one + ) + } + + static private func mapSecResultAndReturnValue( + status: OSStatus, + value: CFTypeRef?, + returnSet: Set<Keychain.Return>, + limit: Keychain.MatchLimit) -> Result<[Keychain.Attributes]> + { + return mapSecResult(status: status) { () -> [Keychain.Attributes] in + return value.map { parseReturnValue(value: $0, returnSet: returnSet, limit: limit) } + ?? [] + } + } + + static private func parseReturnValue( + value: CFTypeRef, + returnSet: Set<Keychain.Return>, + limit: Keychain.MatchLimit) -> [Keychain.Attributes] + { + switch returnSet { + case []: + return [] + + case [.data]: + let values: [Data] = unsafelyCastReturnValue(value: value, limit: limit) + + return values.map { (data) -> Keychain.Attributes in + var attributes = Keychain.Attributes() + attributes.valueData = data + return attributes + } + + case [.persistentReference]: + let values: [Data] = unsafelyCastReturnValue(value: value, limit: limit) + + return values.map { (persistentReference) -> Keychain.Attributes in + var attributes = Keychain.Attributes() + attributes.valuePersistentReference = persistentReference + return attributes + } + + default: + let rawAttributeList: [[CFString: Any]] = + unsafelyCastReturnValue(value: value, limit: limit) + + return rawAttributeList.map { Keychain.Attributes(attributes: $0) } + } + } + + /// A private helper that casts and normalizes the return value from Keychain to produce + /// an array even when a single item is expected to be returned. + static private func unsafelyCastReturnValue<T>( + value: CFTypeRef, + limit: Keychain.MatchLimit) -> [T] + { + switch limit { + case .one: + return [value as! T] + case .all: + return value as! [T] + } + } + + /// A private helper that verifies the given `status` and executes `body` on success + static private func mapSecResult<T>(status: OSStatus, body: () -> T) -> Result<T> { + if status == errSecSuccess { + return .success(body()) + } else { + return .failure(Keychain.Error(code: status)) + } + } +} diff --git a/ios/MullvadVPN/KeychainAttributes.swift b/ios/MullvadVPN/KeychainAttributes.swift new file mode 100644 index 0000000000..397620f37b --- /dev/null +++ b/ios/MullvadVPN/KeychainAttributes.swift @@ -0,0 +1,139 @@ +// +// KeychainAttributes.swift +// MullvadVPN +// +// Created by pronebird on 22/04/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Security + +extension Keychain { + + enum Accessible: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable { + + case whenPasscodeSetThisDeviceOnly + case whenUnlocked + case whenUnlockedThisDeviceOnly + case afterFirstUnlock + case afterFirstUnlockThisDeviceOnly + + var rawValue: CFString { + switch self { + case .whenPasscodeSetThisDeviceOnly: + return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly + case .whenUnlocked: + return kSecAttrAccessibleWhenUnlocked + case .whenUnlockedThisDeviceOnly: + return kSecAttrAccessibleWhenUnlockedThisDeviceOnly + case .afterFirstUnlock: + return kSecAttrAccessibleAfterFirstUnlock + case .afterFirstUnlockThisDeviceOnly: + return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + } + } + + init?(rawValue: CFString) { + let maybeCase = Self.allCases.first { $0.rawValue == rawValue } + + if let maybeCase = maybeCase { + self = maybeCase + } else { + return nil + } + } + + init?(attributes: [CFString: Any]) { + if let rawValue = attributes[kSecAttrAccessible] as? String { + self.init(rawValue: rawValue as CFString) + } else { + return nil + } + } + + func updateKeychainAttributes(in attributes: inout [CFString : Any]) { + attributes[kSecAttrAccessible] = rawValue + } + + } + + struct Attributes: KeychainAttributeEncodable, KeychainAttributeDecodable { + var `class`: KeychainClass? + var service: String? + var account: String? + var accessGroup: String? + var accessible: Accessible? + var creationDate: Date? + var modificationDate: Date? + var generic: Data? + + var valueData: Data? + var valuePersistentReference: Data? + + var `return`: Set<Keychain.Return>? + var matchLimit: Keychain.MatchLimit? + + init() {} + + init(attributes: [CFString: Any]) { + `class` = KeychainClass(attributes: attributes) + service = attributes[kSecAttrService] as? String + account = attributes[kSecAttrAccount] as? String + accessGroup = attributes[kSecAttrAccessGroup] as? String + accessible = Accessible(attributes: attributes) + creationDate = attributes[kSecAttrCreationDate] as? Date + modificationDate = attributes[kSecAttrModificationDate] as? Date + generic = attributes[kSecAttrGeneric] as? Data + + valueData = attributes[kSecValueData] as? Data + valuePersistentReference = attributes[kSecValuePersistentRef] as? Data + + `return` = Set(attributes: attributes) + matchLimit = Keychain.MatchLimit(attributes: attributes) + } + + func updateKeychainAttributes(in attributes: inout [CFString: Any]) { + `class`?.updateKeychainAttributes(in: &attributes) + + if let service = service { + attributes[kSecAttrService] = service + } + + if let account = account { + attributes[kSecAttrAccount] = account + } + + if let accessGroup = accessGroup { + attributes[kSecAttrAccessGroup] = accessGroup + } + + accessible?.updateKeychainAttributes(in: &attributes) + + if let creationDate = creationDate { + attributes[kSecAttrCreationDate] = creationDate + } + + if let modificationDate = modificationDate { + attributes[kSecAttrModificationDate] = modificationDate + } + + if let generic = generic { + attributes[kSecAttrGeneric] = generic + } + + if let valueData = valueData { + attributes[kSecValueData] = valueData + } + + if let valuePersistentReference = valuePersistentReference { + attributes[kSecValuePersistentRef] = valuePersistentReference + } + + `return`?.updateKeychainAttributes(in: &attributes) + matchLimit?.updateKeychainAttributes(in: &attributes) + } + + } + +} diff --git a/ios/MullvadVPN/KeychainClass.swift b/ios/MullvadVPN/KeychainClass.swift new file mode 100644 index 0000000000..86e13f2bd6 --- /dev/null +++ b/ios/MullvadVPN/KeychainClass.swift @@ -0,0 +1,50 @@ +// +// KeychainClass.swift +// MullvadVPN +// +// Created by pronebird on 24/04/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Security + +extension Keychain { + + enum KeychainClass: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable { + case genericPassword + case internetPassword + + var rawValue: CFString { + switch self { + case .genericPassword: + return kSecClassGenericPassword + case .internetPassword: + return kSecClassInternetPassword + } + } + + init?(rawValue: CFString) { + let maybeCase = Self.allCases.first { $0.rawValue == rawValue } + + if let maybeCase = maybeCase { + self = maybeCase + } else { + return nil + } + } + + init?(attributes: [CFString: Any]) { + if let rawValue = attributes[kSecClass] as? String { + self.init(rawValue: rawValue as CFString) + } else { + return nil + } + } + + func updateKeychainAttributes(in attributes: inout [CFString : Any]) { + attributes[kSecClass] = rawValue + } + } + +} diff --git a/ios/MullvadVPN/KeychainError.swift b/ios/MullvadVPN/KeychainError.swift index f006175827..58af6c7ecc 100644 --- a/ios/MullvadVPN/KeychainError.swift +++ b/ios/MullvadVPN/KeychainError.swift @@ -9,21 +9,25 @@ import Foundation import Security -struct KeychainError: Error, LocalizedError { - let code: OSStatus +extension Keychain { + struct Error: Swift.Error, LocalizedError { + let code: OSStatus - var errorDescription: String? { - return SecCopyErrorMessageString(code, nil) as String? + var errorDescription: String? { + return SecCopyErrorMessageString(code, nil) as String? + } } + } -extension KeychainError { - static let duplicateItem = KeychainError(code: errSecDuplicateItem) - static let itemNotFound = KeychainError(code: errSecItemNotFound) +extension Keychain.Error { + + static let duplicateItem = Keychain.Error(code: errSecDuplicateItem) + static let itemNotFound = Keychain.Error(code: errSecItemNotFound) - static func ~= (lhs: KeychainError, rhs: Error) -> Bool { - guard let rhsError = rhs as? KeychainError else { return false } + static func ~= (lhs: Keychain.Error, rhs: Swift.Error) -> Bool { + guard let rhsError = rhs as? Keychain.Error else { return false } return lhs.code == rhsError.code } } diff --git a/ios/MullvadVPN/KeychainMatchLimit.swift b/ios/MullvadVPN/KeychainMatchLimit.swift new file mode 100644 index 0000000000..f86b010532 --- /dev/null +++ b/ios/MullvadVPN/KeychainMatchLimit.swift @@ -0,0 +1,48 @@ +// +// KeychainMatchLimit.swift +// MullvadVPN +// +// Created by pronebird on 24/04/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Security + +extension Keychain { + enum MatchLimit: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable { + case one + case all + + var rawValue: CFString { + switch self { + case .one: + return kSecMatchLimitOne + case .all: + return kSecMatchLimitAll + } + } + + init?(rawValue: CFString) { + let maybeCase = Self.allCases.first { $0.rawValue == rawValue } + + if let maybeCase = maybeCase { + self = maybeCase + } else { + return nil + } + } + + init?(attributes: [CFString : Any]) { + if let rawValue = attributes[kSecMatchLimit] as? String { + self.init(rawValue: rawValue as CFString) + } else { + return nil + } + } + + func updateKeychainAttributes(in attributes: inout [CFString : Any]) { + attributes[kSecMatchLimit] = rawValue + } + } +} diff --git a/ios/MullvadVPN/KeychainReturn.swift b/ios/MullvadVPN/KeychainReturn.swift new file mode 100644 index 0000000000..7216ffbd80 --- /dev/null +++ b/ios/MullvadVPN/KeychainReturn.swift @@ -0,0 +1,57 @@ +// +// KeychainReturn.swift +// MullvadVPN +// +// Created by pronebird on 24/04/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Security + +extension Keychain { + enum Return: KeychainAttributeEncodable, CaseIterable { + case data + case attributes + case persistentReference + + fileprivate var attributeKey: CFString { + switch self { + case .attributes: + return kSecReturnAttributes + case .data: + return kSecReturnData + case .persistentReference: + return kSecReturnPersistentRef + } + } + + func updateKeychainAttributes(in attributes: inout [CFString: Any]) { + attributes[attributeKey] = true + } + } +} + +extension Set: KeychainAttributeDecodable, KeychainAttributeEncodable + where Element == Keychain.Return +{ + init?(attributes: [CFString: Any]) { + let items = Keychain.Return.allCases.filter { (returnType) -> Bool in + return attributes[returnType.attributeKey] as? Bool == .some(true) + } + + if items.isEmpty { + return nil + } else { + self.init(items) + } + } + + func updateKeychainAttributes(in attributes: inout [CFString : Any]) { + Keychain.Return.allCases.forEach { (returnType) in + attributes.removeValue(forKey: returnType.attributeKey) + } + + forEach { $0.updateKeychainAttributes(in: &attributes) } + } +} |
