1
+ import Foundation
2
+ import Security
3
+ import CryptoKit
4
+
5
+ public final class CryptoUtils {
6
+ private static let keyTag = " com.teamtailor.app.cryptoutils "
7
+ private static let keychainGroup = " group.com.teamtailor.keys "
8
+
9
+ public enum CryptoError : Error {
10
+ case keyGenerationFailed( String )
11
+ case keychainAccessFailed( String )
12
+ case publicKeyExtractionFailed
13
+ case privateKeyNotFound
14
+ case invalidBase64Input
15
+ case rsaDecryptionFailed
16
+ case invalidAESKey
17
+ case aesDecryptionFailed
18
+ case rsaEncryptionFailed
19
+ }
20
+
21
+ private static func generateKeyPair( tag: Data ) throws -> SecKey {
22
+ guard let access = SecAccessControlCreateWithFlags (
23
+ kCFAllocatorDefault,
24
+ kSecAttrAccessibleAfterFirstUnlock, // This means that the key is only accessible after the device has been unlocked once. So if we get notifications before that, then we will show fallback gdpr compliant message. Any other option are more restrictive.
25
+ [ ] ,
26
+ nil
27
+ ) else {
28
+ throw CryptoError . keyGenerationFailed ( " Failed to create access control " )
29
+ }
30
+
31
+ let attributes : [ String : Any ] = [
32
+ kSecAttrKeyType as String : kSecAttrKeyTypeRSA,
33
+ kSecAttrKeySizeInBits as String : 2048 ,
34
+ kSecPrivateKeyAttrs as String : [
35
+ kSecAttrIsPermanent as String : true ,
36
+ kSecAttrApplicationTag as String : tag,
37
+ kSecAttrAccessGroup as String : keychainGroup,
38
+ kSecAttrAccessControl as String : access
39
+ ]
40
+ ]
41
+
42
+ guard let privateKey = SecKeyCreateRandomKey ( attributes as CFDictionary , nil ) else {
43
+ throw CryptoError . keyGenerationFailed ( " Failed to generate key pair " )
44
+ }
45
+
46
+ return privateKey
47
+ }
48
+
49
+ private static func getPrivateKey( ) throws -> SecKey {
50
+ let tag = keyTag. data ( using: . utf8) !
51
+ let query : [ String : Any ] = [
52
+ kSecClass as String : kSecClassKey,
53
+ kSecAttrKeyType as String : kSecAttrKeyTypeRSA,
54
+ kSecAttrApplicationTag as String : tag,
55
+ kSecReturnRef as String : true ,
56
+ kSecAttrAccessGroup as String : keychainGroup
57
+ ]
58
+
59
+ var item : CFTypeRef ?
60
+ let status = SecItemCopyMatching ( query as CFDictionary , & item)
61
+
62
+ if status == errSecSuccess {
63
+ return item as! SecKey
64
+ }
65
+
66
+ if status == errSecItemNotFound {
67
+ return try generateKeyPair ( tag: tag)
68
+ }
69
+
70
+ let error = SecCopyErrorMessageString ( status, nil ) as String ? ?? " Unknown error "
71
+ NSLog ( " CryptoUtils: Keychain access failed with error: \( error) " )
72
+ throw CryptoError . keychainAccessFailed ( error)
73
+ }
74
+
75
+ public static func getPublicKey( ) throws -> String {
76
+ let privateKey = try getPrivateKey ( )
77
+
78
+ guard let publicKey = SecKeyCopyPublicKey ( privateKey) else {
79
+ throw CryptoError . publicKeyExtractionFailed
80
+ }
81
+
82
+ guard let publicKeyData = SecKeyCopyExternalRepresentation ( publicKey, nil ) as Data ? else {
83
+ throw CryptoError . publicKeyExtractionFailed
84
+ }
85
+
86
+ return publicKeyData. base64EncodedString ( )
87
+ }
88
+
89
+ public static func rsaDecrypt( encryptedBase64: String ) throws -> Data {
90
+ guard let cipherData = Data ( base64Encoded: encryptedBase64) else {
91
+ throw CryptoError . invalidBase64Input
92
+ }
93
+
94
+ let privateKey = try getPrivateKey ( )
95
+
96
+ var error : Unmanaged < CFError > ?
97
+ guard let decryptedData = SecKeyCreateDecryptedData ( privateKey,
98
+ . rsaEncryptionOAEPSHA1,
99
+ cipherData as CFData ,
100
+ & error) as Data ? else {
101
+ if let error = error? . takeRetainedValue ( ) {
102
+ NSLog ( " RSA Decrypt: Failed with error: \( error) " )
103
+ }
104
+ throw CryptoError . rsaDecryptionFailed
105
+ }
106
+
107
+ return decryptedData
108
+ }
109
+
110
+ public static func rsaEncrypt( data: Data ) throws -> String {
111
+ let privateKey = try getPrivateKey ( )
112
+ guard let publicKey = SecKeyCopyPublicKey ( privateKey) else {
113
+ throw CryptoError . privateKeyNotFound
114
+ }
115
+
116
+ var error : Unmanaged < CFError > ?
117
+ guard let encryptedData = SecKeyCreateEncryptedData (
118
+ publicKey,
119
+ . rsaEncryptionOAEPSHA1,
120
+ data as CFData ,
121
+ & error
122
+ ) as Data ? else {
123
+ if let error = error? . takeRetainedValue ( ) {
124
+ NSLog ( " RSA Encrypt: Failed with error: \( error) " )
125
+ }
126
+ throw CryptoError . rsaEncryptionFailed
127
+ }
128
+
129
+ return encryptedData. base64EncodedString ( )
130
+ }
131
+
132
+ public static func hybridDecrypt(
133
+ encryptedKey: String ,
134
+ cipherText: String ,
135
+ nonce: String ,
136
+ tag: String
137
+ ) throws -> String {
138
+ let aesKeyData = try rsaDecrypt ( encryptedBase64: encryptedKey)
139
+
140
+ guard let cipherData = Data ( base64Encoded: cipherText) ,
141
+ let nonceData = Data ( base64Encoded: nonce) ,
142
+ let tagData = Data ( base64Encoded: tag) else {
143
+ throw CryptoError . invalidBase64Input
144
+ }
145
+
146
+ guard let aeadNonce = try ? AES . GCM. Nonce ( data: nonceData) else {
147
+ throw CryptoError . invalidBase64Input
148
+ }
149
+
150
+ let symmetricKey = SymmetricKey ( data: aesKeyData)
151
+ do {
152
+ let sealedBox = try AES . GCM. SealedBox (
153
+ nonce: aeadNonce,
154
+ ciphertext: cipherData,
155
+ tag: tagData
156
+ )
157
+
158
+ let decryptedData = try AES . GCM. open ( sealedBox, using: symmetricKey)
159
+
160
+ guard let decryptedString = String ( data: decryptedData, encoding: . utf8) else {
161
+ throw CryptoError . aesDecryptionFailed
162
+ }
163
+
164
+ return decryptedString
165
+
166
+ } catch {
167
+ NSLog ( " AES Decryption failed: \( error) " )
168
+ throw CryptoError . aesDecryptionFailed
169
+ }
170
+ }
171
+
172
+ public static func deleteKeyPair( ) {
173
+ let tag = keyTag. data ( using: . utf8) !
174
+ let query : [ String : Any ] = [
175
+ kSecClass as String : kSecClassKey,
176
+ kSecAttrApplicationTag as String : tag,
177
+ kSecAttrAccessGroup as String : keychainGroup
178
+ ]
179
+ SecItemDelete ( query as CFDictionary )
180
+ }
181
+
182
+ public static func testEncryption( message: String ) throws -> Bool {
183
+ guard let messageData = message. data ( using: . utf8) else {
184
+ throw CryptoError . invalidBase64Input
185
+ }
186
+
187
+ let encrypted = try rsaEncrypt ( data: messageData)
188
+ let decryptedData = try rsaDecrypt ( encryptedBase64: encrypted)
189
+
190
+ guard let decryptedMessage = String ( data: decryptedData, encoding: . utf8) else {
191
+ throw CryptoError . rsaDecryptionFailed
192
+ }
193
+
194
+ return message == decryptedMessage
195
+ }
196
+ }
0 commit comments