KeychainManager.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. //
  2. // KeychainManager.swift
  3. // Loop
  4. //
  5. // Created by Nate Racklyeft on 6/26/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import Foundation
  9. import Security
  10. enum KeychainManagerError: Error {
  11. case add(OSStatus)
  12. case copy(OSStatus)
  13. case delete(OSStatus)
  14. case unknownResult
  15. }
  16. /**
  17. Influenced by https://github.com/marketplacer/keychain-swift
  18. */
  19. struct KeychainManager {
  20. typealias Query = [String: NSObject]
  21. var accessibility: CFString = kSecAttrAccessibleAfterFirstUnlock
  22. var accessGroup: String?
  23. struct InternetCredentials {
  24. let username: String
  25. let password: String
  26. let url: URL
  27. }
  28. // MARK: - Convenience methods
  29. private func query(by class: CFString) -> Query {
  30. var query: Query = [kSecClass as String: `class`]
  31. if let accessGroup = accessGroup {
  32. query[kSecAttrAccessGroup as String] = accessGroup as NSObject?
  33. }
  34. return query
  35. }
  36. private func queryForGenericPassword(by service: String) -> Query {
  37. var query = self.query(by: kSecClassGenericPassword)
  38. query[kSecAttrService as String] = service as NSObject?
  39. return query
  40. }
  41. private func queryForInternetPassword(account: String? = nil, url: URL? = nil) -> Query {
  42. var query = self.query(by: kSecClassInternetPassword)
  43. if let account = account {
  44. query[kSecAttrAccount as String] = account as NSObject?
  45. }
  46. if let url = url, let components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
  47. for (key, value) in components.keychainAttributes {
  48. query[key] = value
  49. }
  50. }
  51. return query
  52. }
  53. private func updatedQuery(_ query: Query, withPassword password: String) throws -> Query {
  54. var query = query
  55. guard let value = password.data(using: String.Encoding.utf8) else {
  56. throw KeychainManagerError.add(errSecDecode)
  57. }
  58. query[kSecValueData as String] = value as NSObject?
  59. query[kSecAttrAccessible as String] = accessibility
  60. return query
  61. }
  62. func delete(_ query: Query) throws {
  63. let statusCode = SecItemDelete(query as CFDictionary)
  64. guard statusCode == errSecSuccess || statusCode == errSecItemNotFound else {
  65. throw KeychainManagerError.delete(statusCode)
  66. }
  67. }
  68. // MARK: – Generic Passwords
  69. func replaceGenericPassword(_ password: String?, forService service: String) throws {
  70. var query = queryForGenericPassword(by: service)
  71. try delete(query)
  72. guard let password = password else {
  73. return
  74. }
  75. query = try updatedQuery(query, withPassword: password)
  76. let statusCode = SecItemAdd(query as CFDictionary, nil)
  77. guard statusCode == errSecSuccess else {
  78. throw KeychainManagerError.add(statusCode)
  79. }
  80. }
  81. func getGenericPasswordForService(_ service: String) throws -> String {
  82. var query = queryForGenericPassword(by: service)
  83. query[kSecReturnData as String] = kCFBooleanTrue
  84. query[kSecMatchLimit as String] = kSecMatchLimitOne
  85. var result: AnyObject?
  86. let statusCode = SecItemCopyMatching(query as CFDictionary, &result)
  87. guard statusCode == errSecSuccess else {
  88. throw KeychainManagerError.copy(statusCode)
  89. }
  90. guard let passwordData = result as? Data, let password = String(data: passwordData, encoding: String.Encoding.utf8) else {
  91. throw KeychainManagerError.unknownResult
  92. }
  93. return password
  94. }
  95. // MARK – Internet Passwords
  96. func setInternetPassword(_ password: String, forAccount account: String, atURL url: URL) throws {
  97. var query = try updatedQuery(queryForInternetPassword(account: account, url: url), withPassword: password)
  98. query[kSecAttrAccount as String] = account as NSObject?
  99. if let components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
  100. for (key, value) in components.keychainAttributes {
  101. query[key] = value
  102. }
  103. }
  104. let statusCode = SecItemAdd(query as CFDictionary, nil)
  105. guard statusCode == errSecSuccess else {
  106. throw KeychainManagerError.add(statusCode)
  107. }
  108. }
  109. func replaceInternetCredentials(_ credentials: InternetCredentials?, forAccount account: String) throws {
  110. let query = queryForInternetPassword(account: account)
  111. try delete(query)
  112. if let credentials = credentials {
  113. try setInternetPassword(credentials.password, forAccount: credentials.username, atURL: credentials.url)
  114. }
  115. }
  116. func replaceInternetCredentials(_ credentials: InternetCredentials?, forURL url: URL) throws {
  117. let query = queryForInternetPassword(url: url)
  118. try delete(query)
  119. if let credentials = credentials {
  120. try setInternetPassword(credentials.password, forAccount: credentials.username, atURL: credentials.url)
  121. }
  122. }
  123. func getInternetCredentials(account: String? = nil, url: URL? = nil) throws -> InternetCredentials {
  124. var query = queryForInternetPassword(account: account, url: url)
  125. query[kSecReturnData as String] = kCFBooleanTrue
  126. query[kSecReturnAttributes as String] = kCFBooleanTrue
  127. query[kSecMatchLimit as String] = kSecMatchLimitOne
  128. var result: AnyObject?
  129. let statusCode: OSStatus = SecItemCopyMatching(query as CFDictionary, &result)
  130. guard statusCode == errSecSuccess else {
  131. throw KeychainManagerError.copy(statusCode)
  132. }
  133. if let result = result as? [AnyHashable: Any], let passwordData = result[kSecValueData as String] as? Data,
  134. let password = String(data: passwordData, encoding: String.Encoding.utf8),
  135. let url = URLComponents(keychainAttributes: result)?.url,
  136. let username = result[kSecAttrAccount as String] as? String
  137. {
  138. return InternetCredentials(username: username, password: password, url: url)
  139. }
  140. throw KeychainManagerError.unknownResult
  141. }
  142. }
  143. private enum SecurityProtocol {
  144. case http
  145. case https
  146. init?(scheme: String?) {
  147. switch scheme?.lowercased() {
  148. case "http"?:
  149. self = .http
  150. case "https"?:
  151. self = .https
  152. default:
  153. return nil
  154. }
  155. }
  156. init?(secAttrProtocol: CFString) {
  157. if secAttrProtocol == kSecAttrProtocolHTTP {
  158. self = .http
  159. } else if secAttrProtocol == kSecAttrProtocolHTTPS {
  160. self = .https
  161. } else {
  162. return nil
  163. }
  164. }
  165. var scheme: String {
  166. switch self {
  167. case .http:
  168. return "http"
  169. case .https:
  170. return "https"
  171. }
  172. }
  173. var secAttrProtocol: CFString {
  174. switch self {
  175. case .http:
  176. return kSecAttrProtocolHTTP
  177. case .https:
  178. return kSecAttrProtocolHTTPS
  179. }
  180. }
  181. }
  182. private extension URLComponents {
  183. init?(keychainAttributes: [AnyHashable: Any]) {
  184. self.init()
  185. if let secAttProtocol = keychainAttributes[kSecAttrProtocol as String] {
  186. scheme = SecurityProtocol(secAttrProtocol: secAttProtocol as! CFString)?.scheme
  187. }
  188. host = keychainAttributes[kSecAttrServer as String] as? String
  189. if let port = keychainAttributes[kSecAttrPort as String] as? Int, port > 0 {
  190. self.port = port
  191. }
  192. if let path = keychainAttributes[kSecAttrPath as String] as? String {
  193. self.path = path
  194. }
  195. }
  196. var keychainAttributes: [String: NSObject] {
  197. var query: [String: NSObject] = [:]
  198. if let `protocol` = SecurityProtocol(scheme: scheme) {
  199. query[kSecAttrProtocol as String] = `protocol`.secAttrProtocol
  200. }
  201. if let host = host {
  202. query[kSecAttrServer as String] = host as NSObject
  203. }
  204. if let port = port {
  205. query[kSecAttrPort as String] = port as NSObject
  206. }
  207. if !path.isEmpty {
  208. query[kSecAttrPath as String] = path as NSObject
  209. }
  210. return query
  211. }
  212. }