55import Foundation
66
77class IterableKeychain {
8+ init ( wrapper: KeychainWrapper = KeychainWrapper ( ) ) {
9+ self . wrapper = wrapper
10+ }
11+
812 var email : String ? {
913 get {
1014 let data = wrapper. data ( forKey: Const . Keychain. Key. email)
@@ -21,7 +25,6 @@ class IterableKeychain {
2125
2226 wrapper. set ( data, forKey: Const . Keychain. Key. email)
2327 }
24-
2528 }
2629
2730 var userId : String ? {
@@ -48,6 +51,7 @@ class IterableKeychain {
4851
4952 return data. flatMap { String ( data: $0, encoding: . utf8) }
5053 }
54+
5155 set {
5256 guard let token = newValue,
5357 let data = token. data ( using: . utf8) else {
@@ -59,134 +63,104 @@ class IterableKeychain {
5963 }
6064 }
6165
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)
6477 }
6578
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)
7486 }
7587
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+ }
8197
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+ }
84102
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+ }
86107
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
93110 }
111+
112+ return ( payload: lastPushPayloadData, expiration: payloadExpirationPair. expiration)
94113 }
95114
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+ }
98120
99- // Limit search results to one
100- keychainQueryDictionary [ SecMatchLimit] = SecMatchLimitOne
121+ guard let payloadAsData = encodeJsonPayload ( payload) else {
122+ return
123+ }
101124
102- // Specify we want Data/CFData returned
103- keychainQueryDictionary [ SecReturnData] = CFBooleanTrue
125+ let payloadExpirationPair = LastPushPayloadValue ( payload: payloadAsData, expiration: expiration)
104126
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+ }
108130
109- return status == noErr ? result as? Data : nil
131+ wrapper . set ( encodedPair , forKey : Const . Keychain . Key . lastPushPayloadAndExpiration )
110132 }
111133
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
123137 }
138+
139+ return try ? JSONSerialization . data ( withJSONObject: json)
124140 }
125141
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
138145 }
146+
147+ return try ? JSONSerialization . jsonObject ( with: data) as? [ AnyHashable : Any ]
139148 }
140149
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)
161152 }
162153
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 {
173156 return false
174157 }
158+
159+ return !( expiration. timeIntervalSinceReferenceDate > currentDate. timeIntervalSinceReferenceDate)
175160 }
176161
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+ }
191166}
192-
0 commit comments