Skip to content

Commit df51e64

Browse files
authored
Merge pull request #579 from Iterable/encryption
merging encryption feature into main SDK
2 parents 9773e60 + c23ab0b commit df51e64

28 files changed

+1232
-668
lines changed

swift-sdk.xcodeproj/project.pbxproj

Lines changed: 280 additions & 4 deletions
Large diffs are not rendered by default.

swift-sdk/Constants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ enum Const {
6363
static let email = "itbl_email"
6464
static let userId = "itbl_userid"
6565
static let authToken = "itbl_auth_token"
66+
static let lastPushPayloadAndExpiration = "itbl_last_push_payload_and_expiration"
6667
}
6768
}
6869

swift-sdk/Internal/InternalIterableAPI.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
4141
}
4242

4343
var lastPushPayload: [AnyHashable: Any]? {
44-
localStorage.getPayload(currentDate: dateProvider.currentDate)
44+
localStorage.getLastPushPayload(dateProvider.currentDate)
4545
}
4646

4747
var attributionInfo: IterableAttributionInfo? {
@@ -522,7 +522,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
522522
let expiration = Calendar.current.date(byAdding: .hour,
523523
value: Const.UserDefault.payloadExpiration,
524524
to: dateProvider.currentDate)
525-
localStorage.save(payload: payload, withExpiration: expiration)
525+
localStorage.saveLastPushPayload(payload, withExpiration: expiration)
526526

527527
if let metadata = IterablePushNotificationMetadata.metadata(fromLaunchOptions: payload) {
528528
if let templateId = metadata.templateId {

swift-sdk/Internal/IterableKeychain.swift

Lines changed: 77 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import Foundation
66

77
class 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-

swift-sdk/Internal/IterableUserDefaults.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class IterableUserDefaults {
1010
self.userDefaults = userDefaults
1111
}
1212

13+
// migrated to IterableKeychain
1314
var userId: String? {
1415
get {
1516
string(withKey: .userId)
@@ -18,6 +19,7 @@ class IterableUserDefaults {
1819
}
1920
}
2021

22+
// migrated to IterableKeychain
2123
var email: String? {
2224
get {
2325
string(withKey: .email)
@@ -26,6 +28,7 @@ class IterableUserDefaults {
2628
}
2729
}
2830

31+
// migrated to IterableKeychain
2932
var authToken: String? {
3033
get {
3134
string(withKey: .authToken)
@@ -34,6 +37,7 @@ class IterableUserDefaults {
3437
}
3538
}
3639

40+
// deprecated, not in use anymore
3741
var ddlChecked: Bool {
3842
get {
3943
bool(withKey: .ddlChecked)
@@ -74,14 +78,37 @@ class IterableUserDefaults {
7478
try? save(codable: attributionInfo, withKey: .attributionInfo, andExpiration: expiration)
7579
}
7680

81+
// migrated to IterableKeychain
7782
func getPayload(currentDate: Date) -> [AnyHashable: Any]? {
7883
(try? dict(withKey: .payload, currentDate: currentDate)) ?? nil
7984
}
8085

86+
// migrated to IterableKeychain
8187
func save(payload: [AnyHashable: Any]?, withExpiration expiration: Date?) {
8288
try? save(dict: payload, withKey: .payload, andExpiration: expiration)
8389
}
8490

91+
// MARK: data migration functions
92+
93+
func getLastPushPayloadExpirationPairForMigration() -> (payload: [AnyHashable: Any]?, expiration: Date?)? {
94+
guard let encodedEnvelope = userDefaults.value(forKey: UserDefaultsKey.payload.value) as? Data else {
95+
return nil
96+
}
97+
98+
do {
99+
let envelope = try JSONDecoder().decode(Envelope.self, from: encodedEnvelope)
100+
let decoded = try JSONSerialization.jsonObject(with: envelope.payload, options: []) as? [AnyHashable: Any]
101+
102+
return (payload: decoded, envelope.expiration)
103+
} catch {
104+
return nil
105+
}
106+
}
107+
108+
func getAuthDataForMigration() -> (email: String?, userId: String?, authToken: String?) {
109+
return (email: email, userId: userId, authToken: authToken)
110+
}
111+
85112
// MARK: Private implementation
86113

87114
private let userDefaults: UserDefaults

0 commit comments

Comments
 (0)