summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-05-11 18:27:44 +0200
committerAndrej Mihajlov <and@mullvad.net>2020-05-11 18:27:44 +0200
commit5151488fcd871f84a8d6955225d5da5d0155e347 (patch)
treeb9b2579c73f2109e6077799c70966f0d76540682
parent70e1e7e48b8a49a7e227ca538dfa56a309791344 (diff)
parent5a029132f5f640f39a1fb908732bb3654d23c936 (diff)
downloadmullvadvpn-5151488fcd871f84a8d6955225d5da5d0155e347.tar.xz
mullvadvpn-5151488fcd871f84a8d6955225d5da5d0155e347.zip
Merge branch 'add-keychain-abstractions'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj32
-rw-r--r--ios/MullvadVPN/Keychain.swift154
-rw-r--r--ios/MullvadVPN/KeychainAttributes.swift139
-rw-r--r--ios/MullvadVPN/KeychainClass.swift50
-rw-r--r--ios/MullvadVPN/KeychainError.swift22
-rw-r--r--ios/MullvadVPN/KeychainMatchLimit.swift48
-rw-r--r--ios/MullvadVPN/KeychainReturn.swift57
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) }
+ }
+}