5
5
import Foundation
6
6
7
7
class IterableKeychain {
8
+ init ( wrapper: KeychainWrapper = KeychainWrapper ( ) ) {
9
+ self . wrapper = wrapper
10
+ }
11
+
8
12
var email : String ? {
9
13
get {
10
14
let data = wrapper. data ( forKey: Const . Keychain. Key. email)
@@ -21,7 +25,6 @@ class IterableKeychain {
21
25
22
26
wrapper. set ( data, forKey: Const . Keychain. Key. email)
23
27
}
24
-
25
28
}
26
29
27
30
var userId : String ? {
@@ -48,6 +51,7 @@ class IterableKeychain {
48
51
49
52
return data. flatMap { String ( data: $0, encoding: . utf8) }
50
53
}
54
+
51
55
set {
52
56
guard let token = newValue,
53
57
let data = token. data ( using: . utf8) else {
@@ -59,134 +63,104 @@ class IterableKeychain {
59
63
}
60
64
}
61
65
62
- init ( wrapper: KeychainWrapper = KeychainWrapper ( ) ) {
63
- self . wrapper = wrapper
66
+ func getLastPushPayload( currentDate: Date ) -> [ AnyHashable : Any ] ? {
67
+ guard let payloadExpirationPair = getPayloadExpirationPairFromKeychain ( ) else {
68
+ return nil
69
+ }
70
+
71
+ if isLastPushPayloadExpired ( expiration: payloadExpirationPair. expiration, currentDate: currentDate) {
72
+ removePayloadExpirationPairFromKeychain ( )
73
+ return nil
74
+ }
75
+
76
+ return decodeJsonPayload ( payloadExpirationPair. payload)
64
77
}
65
78
66
- private let wrapper : KeychainWrapper
67
- }
68
-
69
- /// Basic wrapper for keychain
70
- /// This should have no dependency on Iterable classes
71
- class KeychainWrapper {
72
- init ( serviceName: String = Const . Keychain. serviceName) {
73
- self . serviceName = serviceName
79
+ func setLastPushPayload( _ payload: [ AnyHashable : Any ] ? , withExpiration expiration: Date ? ) {
80
+ guard let payload = payload, JSONSerialization . isValidJSONObject ( payload) else {
81
+ removePayloadExpirationPairFromKeychain ( )
82
+ return
83
+ }
84
+
85
+ savePayloadExpirationPairToKeychain ( payload: payload, expiration: expiration)
74
86
}
75
87
76
- @discardableResult
77
- func set( _ value: Data , forKey key: String ) -> Bool {
78
- var keychainQueryDictionary : [ String : Any ] = setupKeychainQueryDictionary ( forKey: key)
79
-
80
- keychainQueryDictionary [ SecValueData] = value
88
+ // MARK: - PRIVATE/INTERNAL
89
+
90
+ private let wrapper : KeychainWrapper
91
+
92
+ private func getPayloadExpirationPairFromKeychain( ) -> ( payload: Data , expiration: Date ? ) ? {
93
+ // get the value from the keychain
94
+ guard let keychainValue = wrapper. data ( forKey: Const . Keychain. Key. lastPushPayloadAndExpiration) else {
95
+ return nil
96
+ }
81
97
82
- // Assign default protection - Protect the keychain entry so it's only valid when the device is unlocked
83
- keychainQueryDictionary [ SecAttrAccessible] = SecAttrAccessibleWhenUnlocked
98
+ // decode the payload/expiration pair
99
+ guard let payloadExpirationPair = try ? JSONDecoder ( ) . decode ( LastPushPayloadValue . self, from: keychainValue) else {
100
+ return nil
101
+ }
84
102
85
- let status : OSStatus = SecItemAdd ( keychainQueryDictionary as CFDictionary , nil )
103
+ // cast the payload as a JSON object
104
+ guard let lastPushPayloadJSON = try ? JSONSerialization . jsonObject ( with: payloadExpirationPair. payload, options: [ ] ) as? [ AnyHashable : Any ] else {
105
+ return nil
106
+ }
86
107
87
- if status == errSecSuccess {
88
- return true
89
- } else if status == errSecDuplicateItem {
90
- return update ( value, forKey: key)
91
- } else {
92
- return false
108
+ guard let lastPushPayloadData = try ? JSONSerialization . data ( withJSONObject: lastPushPayloadJSON) else {
109
+ return nil
93
110
}
111
+
112
+ return ( payload: lastPushPayloadData, expiration: payloadExpirationPair. expiration)
94
113
}
95
114
96
- func data( forKey key: String ) -> Data ? {
97
- var keychainQueryDictionary = setupKeychainQueryDictionary ( forKey: key)
115
+ private func savePayloadExpirationPairToKeychain( payload: [ AnyHashable : Any ] ? , expiration: Date ? ) {
116
+ guard let payload = payload else {
117
+ removePayloadExpirationPairFromKeychain ( )
118
+ return
119
+ }
98
120
99
- // Limit search results to one
100
- keychainQueryDictionary [ SecMatchLimit] = SecMatchLimitOne
121
+ guard let payloadAsData = encodeJsonPayload ( payload) else {
122
+ return
123
+ }
101
124
102
- // Specify we want Data/CFData returned
103
- keychainQueryDictionary [ SecReturnData] = CFBooleanTrue
125
+ let payloadExpirationPair = LastPushPayloadValue ( payload: payloadAsData, expiration: expiration)
104
126
105
- // Search
106
- var result : AnyObject ?
107
- let status = SecItemCopyMatching ( keychainQueryDictionary as CFDictionary , & result )
127
+ guard let encodedPair = try ? JSONEncoder ( ) . encode ( payloadExpirationPair ) else {
128
+ return
129
+ }
108
130
109
- return status == noErr ? result as? Data : nil
131
+ wrapper . set ( encodedPair , forKey : Const . Keychain . Key . lastPushPayloadAndExpiration )
110
132
}
111
133
112
- @discardableResult
113
- func removeValue( forKey key: String ) -> Bool {
114
- let keychainQueryDictionary : [ String : Any ] = setupKeychainQueryDictionary ( forKey: key)
115
-
116
- // Delete
117
- let status : OSStatus = SecItemDelete ( keychainQueryDictionary as CFDictionary )
118
-
119
- if status == errSecSuccess {
120
- return true
121
- } else {
122
- return false
134
+ private func encodeJsonPayload( _ json: [ AnyHashable : Any ] ? ) -> Data ? {
135
+ guard let json = json, JSONSerialization . isValidJSONObject ( json) else {
136
+ return nil
123
137
}
138
+
139
+ return try ? JSONSerialization . data ( withJSONObject: json)
124
140
}
125
141
126
- @discardableResult
127
- func removeAll( ) -> Bool {
128
- var keychainQueryDictionary : [ String : Any ] = [ SecClass: SecClassGenericPassword]
129
-
130
- keychainQueryDictionary [ SecAttrService] = serviceName
131
-
132
- let status : OSStatus = SecItemDelete ( keychainQueryDictionary as CFDictionary )
133
-
134
- if status == errSecSuccess {
135
- return true
136
- } else {
137
- return false
142
+ private func decodeJsonPayload( _ data: Data ? ) -> [ AnyHashable : Any ] ? {
143
+ guard let data = data else {
144
+ return nil
138
145
}
146
+
147
+ return try ? JSONSerialization . jsonObject ( with: data) as? [ AnyHashable : Any ]
139
148
}
140
149
141
-
142
- private let serviceName : String
143
-
144
- private func setupKeychainQueryDictionary( forKey key: String ) -> [ String : Any ] {
145
- // Setup default access as generic password (rather than a certificate, internet password, etc)
146
- var keychainQueryDictionary : [ String : Any ] = [ SecClass: SecClassGenericPassword]
147
-
148
- // Uniquely identify this keychain accessor
149
- keychainQueryDictionary [ SecAttrService] = serviceName
150
-
151
- // Uniquely identify the account who will be accessing the keychain
152
- let encodedIdentifier : Data ? = key. data ( using: . utf8)
153
-
154
- keychainQueryDictionary [ SecAttrGeneric] = encodedIdentifier
155
-
156
- keychainQueryDictionary [ SecAttrAccount] = encodedIdentifier
157
-
158
- keychainQueryDictionary [ SecAttrSynchronizable] = CFBooleanFalse
159
-
160
- return keychainQueryDictionary
150
+ private func removePayloadExpirationPairFromKeychain( ) {
151
+ wrapper. removeValue ( forKey: Const . Keychain. Key. lastPushPayloadAndExpiration)
161
152
}
162
153
163
- private func update( _ value: Data , forKey key: String ) -> Bool {
164
- let keychainQueryDictionary : [ String : Any ] = setupKeychainQueryDictionary ( forKey: key)
165
- let updateDictionary = [ SecValueData: value]
166
-
167
- // Update
168
- let status : OSStatus = SecItemUpdate ( keychainQueryDictionary as CFDictionary , updateDictionary as CFDictionary )
169
-
170
- if status == errSecSuccess {
171
- return true
172
- } else {
154
+ private func isLastPushPayloadExpired( expiration: Date ? , currentDate: Date ) -> Bool {
155
+ guard let expiration = expiration else {
173
156
return false
174
157
}
158
+
159
+ return !( expiration. timeIntervalSinceReferenceDate > currentDate. timeIntervalSinceReferenceDate)
175
160
}
176
161
177
- private let SecValueData = kSecValueData as String
178
- private let SecAttrAccessible : String = kSecAttrAccessible as String
179
- private let SecAttrAccessibleWhenUnlocked = kSecAttrAccessibleWhenUnlocked
180
- private let SecClass : String = kSecClass as String
181
- private let SecClassGenericPassword = kSecClassGenericPassword
182
- private let SecAttrService : String = kSecAttrService as String
183
- private let SecAttrGeneric : String = kSecAttrGeneric as String
184
- private let SecAttrAccount : String = kSecAttrAccount as String
185
- private let SecAttrSynchronizable : String = kSecAttrSynchronizable as String
186
- private let CFBooleanTrue = kCFBooleanTrue
187
- private let CFBooleanFalse = kCFBooleanFalse
188
- private let SecMatchLimit : String = kSecMatchLimit as String
189
- private let SecMatchLimitOne = kSecMatchLimitOne
190
- private let SecReturnData : String = kSecReturnData as String
162
+ private struct LastPushPayloadValue : Codable {
163
+ let payload : Data
164
+ let expiration : Date ?
165
+ }
191
166
}
192
-
0 commit comments