Line data Source code
1 : // 2 : // SwiftySimpleKeychain+RSAHelper 3 : // SwiftySimpleKeychain_RSAHelper 4 : // 5 : // Copyright (c) 2022 Ezequiel (Kimi) Aceto (https://eaceto.dev). Based on Auth0's SimpleKeychain 6 : // 7 : // Permission is hereby granted, free of charge, to any person obtaining a copy 8 : // of this software and associated documentation files (the "Software"), to deal 9 : // in the Software without restriction, including without limitation the rights 10 : // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 : // copies of the Software, and to permit persons to whom the Software is 12 : // furnished to do so, subject to the following conditions: 13 : // 14 : // The above copyright notice and this permission notice shall be included in 15 : // all copies or substantial portions of the Software. 16 : // 17 : // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 : // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 : // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 : // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 : // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 : // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 : // THE SOFTWARE. 24 : 25 : import SwiftySimpleKeychain 26 : 27 : #if canImport(Foundation) 28 : import Foundation 29 : #endif 30 : 31 : #if canImport(Security) 32 : import Security 33 : #endif 34 : 35 : /** 36 : * An extension of SwiftySimpleKeychain to deal with generation, retrieving and deletion of 37 : * public / private RSA keys using the Keychain. 38 : * It honours the configuration (and access control) of **SwiftySimpleKeychain** 39 : */ 40 : public extension SwiftySimpleKeychain { 41 : 42 : /** 43 : * Generates a RSA key pair with the specified key size and stores it in the Keychain 44 : * 45 : * Usage: 46 : * ```swift 47 : * keychain = SwiftySimpleKeychain(with: "service-name") 48 : * let result = keychain.generateRSAKeyPair() 49 : * if case let .failure(error) = result { 50 : * print(error) 51 : * } 52 : * ``` 53 : * 54 : * - Parameters: 55 : * - parameter length: the length (int bits) of the RSA keys. Defaults to 2048 bits 56 : * - parameter publicKeyTag: a tag (identifier) for the public key. Defaults to *public* 57 : * - parameter privateKeyTag: a tag (identifier) for the private key. Defaults to *private* 58 : * - return a **VoidErrorResult** 59 : */ 60 : func generateRSAKeyPair(with length: SwiftySimpleKeychainRSAKeySize = .size2048Bits, 61 : publicKeyTag: String = "public", 62 : privateKeyTag : String = "private" 63 9 : ) -> VoidErrorResult { 64 9 : if (publicKeyTag.isEmpty || privateKeyTag.isEmpty) { 65 2 : return .failure(.wrongParameter) 66 7 : } 67 7 : 68 7 : guard let publicKeyTagData = publicKeyTag.data(using: .utf8) else { 69 0 : return .failure(.decode) 70 7 : } 71 7 : 72 7 : guard let privateKeyTagData = privateKeyTag.data(using: .utf8) else { 73 0 : return .failure(.decode) 74 7 : } 75 7 : 76 7 : var pairAttr = [ 77 7 : kSecAttrKeyType as String: kSecAttrKeyTypeRSA, 78 7 : kSecAttrKeySizeInBits as String: length.rawValue, 79 7 : ] as [String:Any] 80 7 : 81 7 : let privateAttr = [ 82 7 : kSecAttrIsPermanent as String: true, 83 7 : kSecAttrApplicationTag as String: privateKeyTagData, 84 7 : ] as [String:Any] 85 7 : 86 7 : let publicAttr = [ 87 7 : kSecAttrIsPermanent as String: true, 88 7 : kSecAttrApplicationTag as String: publicKeyTagData, 89 7 : ] as [String:Any] 90 7 : 91 7 : pairAttr[kSecPrivateKeyAttrs as String] = privateAttr 92 7 : pairAttr[kSecPublicKeyAttrs as String] = publicAttr 93 7 : 94 7 : var publicKeyRef : SecKey? 95 7 : var privateKeyRef : SecKey? 96 7 : 97 7 : let status = SecKeyGeneratePair(pairAttr as CFDictionary, &publicKeyRef, &privateKeyRef); 98 7 : 99 7 : if status == errSecSuccess { 100 7 : return .success(()) 101 7 : } 102 0 : return .failure(SwiftySimpleKeychainError.from(status)) 103 7 : } 104 : 105 : /** 106 : * Finds a Key stored in the **Keychain** and returns a **DataErrorResult** 107 : * Use this method if you would like to know why fetching a key fails, and an error 108 : * is present in the *failure* response. 109 : * 110 : * Usage: 111 : * ```swift 112 : * let result = keychain.rsaKeyDataResult(with: "publicKeyTag") 113 : * guard case let .success(publicKeyData) = publicKeyDataResult else { 114 : * if case let .failure(error) = publicKeyDataResult { 115 : * print(error) 116 : * fail("Fail to get Public Key. \(error)") 117 : * } 118 : * return 119 : * } 120 : * ``` 121 : * 122 : * - Parameters: 123 : * - parameter tag: a tag (identifier) for the key. 124 : * - return a **DataErrorResult** 125 : */ 126 6 : func rsaKeyDataResult(with tag: String) -> DataErrorResult { 127 6 : let queryResult = keyQuery(with: tag) 128 6 : var query: [String:Any] 129 6 : 130 6 : switch queryResult { 131 6 : case .success(let dict): query = dict 132 6 : case .failure(let err): return .failure(err) 133 6 : } 134 6 : 135 6 : query[kSecReturnData as String] = true 136 6 : 137 6 : var dataRef: CFTypeRef? 138 6 : let status = SecItemCopyMatching(query as CFDictionary, &dataRef) 139 6 : 140 6 : if status != errSecSuccess { 141 0 : return .failure(SwiftySimpleKeychainError.from(status)) 142 6 : } 143 6 : 144 6 : guard let data = dataRef as? Data else { 145 0 : return .failure(SwiftySimpleKeychainError.allocation) 146 6 : } 147 6 : return .success(data) 148 6 : } 149 : 150 : /** 151 : * Finds a Key stored in the **Keychain** and returns a **Data** if found 152 : * Use this method if you want a simple response (data or zero) regardless of error on failure 153 : * 154 : * Usage: 155 : * ```swift 156 : * let data = keychain.rsaKeyData(with: "publicKeyTag") 157 : * ``` 158 : * 159 : * - Parameters: 160 : * - parameter tag: a tag (identifier) for the key. 161 : * - return a **Data** if found. If not, returns nil 162 : */ 163 4 : func rsaKeyData(with tag: String) -> Data? { 164 4 : let result = rsaKeyDataResult(with: tag) 165 4 : switch result { 166 4 : case .success(let data): return data 167 4 : case .failure(_ ): return nil 168 4 : } 169 4 : } 170 : 171 : /** 172 : * Cheks if there is a key with the provided **tag** 173 : * 174 : * - Parameters: 175 : * - parameter tag: a tag (identifier) for the key. 176 : * - return **true** if the key exists 177 : */ 178 4 : func hasRSAKey(with tag: String) -> Bool { 179 4 : let queryResult = keyQuery(with: tag) 180 4 : var query: [String:Any] 181 4 : 182 4 : switch queryResult { 183 4 : case .success(let dict): query = dict 184 4 : case .failure(_): return false 185 4 : } 186 4 : 187 4 : query[kSecReturnData as String] = false 188 4 : 189 4 : let status = SecItemCopyMatching(query as CFDictionary, nil) 190 4 : return status == errSecSuccess 191 4 : } 192 : 193 : /** 194 : * Deletes a RSA Key identified with the provided **tag** 195 : * 196 : * Usage: 197 : * ```swift 198 : * let deleted = keychain.deleteRSAKey(with: "publicKeyTag") 199 : * ``` 200 : * 201 : * - Parameters: 202 : * - parameter tag: a tag (identifier) for the key. 203 : * - return **true** if the key exists and was deleted 204 : */ 205 14 : func deleteRSAKey(with tag: String) -> Bool { 206 14 : let queryResult = keyQuery(with: tag) 207 14 : var query: [String:Any] 208 14 : 209 14 : switch queryResult { 210 14 : case .success(let dict): query = dict 211 14 : case .failure(_): return false 212 14 : } 213 14 : 214 14 : let status = SecItemDelete(query as CFDictionary) 215 14 : return status == errSecSuccess 216 14 : } 217 : 218 : /** 219 : * Finds a Key stored in the **Keychain** and returns a **SecKeyErrorResult** 220 : * 221 : * ```swift 222 : * let result = keychain.rsaSecKeyResult(with: "publicTag") 223 : * if case let .failure(error) = result { 224 : * print(error) 225 : * } 226 : * ``` 227 : * 228 : * - Parameters: 229 : * - parameter tag: a tag (identifier) for the key. 230 : * - return a **SecKeyErrorResult** 231 : */ 232 4 : func rsaSecKeyResult(with tag: String) -> SecKeyErrorResult { 233 4 : let queryResult = keyQuery(with: tag) 234 4 : var query: [String:Any] 235 4 : 236 4 : switch queryResult { 237 4 : case .success(let dict): query = dict 238 4 : case .failure(let err): return .failure(err) 239 4 : } 240 4 : 241 4 : query[kSecReturnData as String] = true 242 4 : 243 4 : var result: CFTypeRef? 244 4 : let status = SecItemCopyMatching(query as CFDictionary, &result) 245 4 : if status == errSecSuccess { 246 4 : return .success(result as! SecKey) 247 4 : } 248 0 : return .failure(SwiftySimpleKeychainError.from(status)) 249 4 : } 250 : 251 : /** 252 : * Finds a Key stored in the **Keychain** and returns a **SecKeyErrorResult** 253 : * 254 : * ```swift 255 : * if let publicKey = keychain.rsaSecKey(with: "publicTag") { 256 : * 257 : * } 258 : * ``` 259 : * 260 : * - Parameters: 261 : * - parameter tag: a tag (identifier) for the key. 262 : * - return a **SecKey** if the RSA key exists. 263 : */ 264 2 : func rsaSecKey(with tag: String) -> SecKey? { 265 2 : switch rsaSecKeyResult(with: tag) { 266 2 : case .success(let secKey): return secKey 267 2 : case .failure: return nil 268 2 : } 269 2 : } 270 : } 271 : 272 : extension SwiftySimpleKeychain { 273 29 : internal func keyQuery(with tag: String) -> Result<[String:Any],SwiftySimpleKeychainError> { 274 29 : if (tag.isEmpty) { 275 1 : return .failure(.wrongParameter) 276 28 : } 277 28 : 278 28 : guard let tagData = tag.data(using: .utf8) else { 279 0 : return .failure(.decode) 280 28 : } 281 28 : 282 28 : return .success([ 283 28 : kSecClass as String: kSecClassKey, 284 28 : kSecAttrApplicationTag as String: tagData, 285 28 : kSecAttrType as String: kSecAttrKeyTypeRSA 286 28 : ] as [String:Any]) 287 28 : } 288 : }