Skip to content

Commit 56258b4

Browse files
authored
Validate and Hash CTID (#531)
* Hash CTID and userId * Fix ctid when anonymous * Add tests * Use userId hash for anonymousLoginUserId
1 parent b99f0f2 commit 56258b4

File tree

7 files changed

+387
-43
lines changed

7 files changed

+387
-43
lines changed

LeanplumSDK/LeanplumSDK/ClassesSwift/Migration/Wrapper/CTWrapper.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class CTWrapper: Wrapper {
2121
static let iOSTransactionIdentifierParam = "iOSTransactionIdentifier"
2222
static let iOSReceiptDataParam = "iOSReceiptData"
2323
static let iOSSandboxParam = "iOSSandbox"
24+
25+
static let DevicesUserProperty = "devices"
2426
}
2527

2628
// MARK: Initialization
@@ -67,6 +69,8 @@ class CTWrapper: Wrapper {
6769
""")
6870
cleverTapInstance?.onUserLogin(identityManager.profile,
6971
withCleverTapID: identityManager.cleverTapID)
72+
73+
setDevicesProperty()
7074
}
7175
triggerInstanceCallback()
7276
}
@@ -196,6 +200,8 @@ class CTWrapper: Wrapper {
196200
and CleverTapID: \(cleverTapID)")
197201
""")
198202
cleverTapInstance?.onUserLogin(profile, withCleverTapID: cleverTapID)
203+
204+
setDevicesProperty()
199205
}
200206

201207
func setPushToken(_ token: Data) {
@@ -210,6 +216,11 @@ class CTWrapper: Wrapper {
210216
}
211217
}
212218

219+
func setDevicesProperty() {
220+
// CleverTap SDK ensures the values are unique locally
221+
cleverTapInstance?.profileAddMultiValue(identityManager.deviceId, forKey: Constants.DevicesUserProperty)
222+
}
223+
213224
// MARK: Traffic Source
214225
func setTrafficSourceInfo(_ info: [AnyHashable: Any]) {
215226
let source = info["publisherName"] as? String

LeanplumSDK/LeanplumSDK/ClassesSwift/Migration/Wrapper/IdentityManager.swift

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@
66
// Copyright © 2022 Leanplum. All rights reserved.
77

88
import Foundation
9+
// Use @_implementationOnly to *not* expose CleverTapSDK to the Leanplum-Swift header
10+
@_implementationOnly import CleverTapSDK
911

1012
/**
1113
* Identity mapping between Leanplum userId and deviceId and CleverTap Identity and CTID.
1214
*
1315
* Mappings:
1416
* - anonymous: <CTID=deviceId, Identity=null>
15-
* - non-anonymous to <CTID=deviceId_userId, Identity=userId>
17+
* - non-anonymous to <CTID=deviceId_userIdHash, Identity=userId>
18+
* - if invalid deviceId <deviceId=deviceIdHash>
19+
*
20+
* UserId Hash is generated using the first 10 chars of the userId SHA256 string (hex).
21+
* If the deviceId does _not_ pass validation _or_ is _longer_ than 50 characters,
22+
* the first 32 chars of the deviceId SHA256 string (hex) are used as deviceIdHash.
1623
*
1724
* - Note: On login of anonymous user, a merge should happen. CleverTap SDK allows merges
1825
* only when the CTID remains the same, meaning that the merged profile would get the anonymous
19-
* profile's CTID: <CTID=deviceId, Identity=userId>.
26+
* profile's CTID: <CTID=deviceId, Identity=userIdHash>.
2027
* In order to keep track which is that userId, it is saved into `anonymousLoginUserId`.
2128
* For this userId, the CTID is always set to deviceId.
2229
* Leanplum UserId can be set through Leanplum.start and Leanplum.setUserId
@@ -29,6 +36,9 @@ class IdentityManager {
2936
static let Identity = "Identity"
3037
static let AnonymousLoginUserIdKey = "__leanplum_anonymous_login_user_id"
3138
static let IdentityStateKey = "__leanplum_identity_state"
39+
40+
static let DeviceIdLengthLimit = 50
41+
static let IdentityHashLength = 10
3242
}
3343

3444
enum IdentityState: String {
@@ -58,7 +68,12 @@ class IdentityManager {
5868

5969
func setUserId(_ userId: String) {
6070
if (state == IdentityState.anonymous()) {
61-
anonymousLoginUserId = userId
71+
if let hash = Utilities.sha256_40(string: userId) {
72+
anonymousLoginUserId = hash
73+
} else {
74+
Log.error("[Wrapper] Failed to generate SHA256 for userId: \(userId)")
75+
anonymousLoginUserId = userIdHash
76+
}
6277
Log.debug("[Wrapper] Anonymous user on device \(deviceId) will be merged to \(userId)")
6378
state = IdentityState.identified()
6479
}
@@ -76,19 +91,51 @@ class IdentityManager {
7691
func identifyNonAnonymous() {
7792
if let state = state,
7893
state == IdentityState.anonymous() {
79-
anonymousLoginUserId = userId
94+
anonymousLoginUserId = userIdHash
8095
}
8196
state = IdentityState.identified()
8297
}
8398

84-
var cleverTapID: String {
85-
if userId != anonymousLoginUserId,
86-
userId != deviceId {
87-
return "\(deviceId)_\(userId)"
99+
var userIdHash: String {
100+
guard let hash = Utilities.sha256_40(string: userId) else {
101+
Log.error("[Wrapper] Failed to generate SHA256 for userId: \(userId)")
102+
return userId
88103
}
89-
104+
105+
return hash
106+
}
107+
108+
var isValidCleverTapID: Bool {
109+
// Only the deviceId could be invalid, since the userIdHash should always be valid,
110+
// but we still validate the whole CTID to be safe
111+
CleverTap.isValidCleverTapId(originalCleverTapID) &&
112+
deviceId.count <= Constants.DeviceIdLengthLimit
113+
}
114+
115+
var originalCleverTapID: String {
116+
if shouldAppendUserId {
117+
return "\(deviceId)_\(userIdHash)"
118+
}
119+
90120
return deviceId
91121
}
122+
123+
var cleverTapID: String {
124+
if isValidCleverTapID {
125+
return originalCleverTapID
126+
}
127+
128+
guard let ctDevice = Utilities.sha256_128(string: deviceId) else {
129+
Log.error("[Wrapper] Failed to generate SHA256 for deviceId: \(deviceId)")
130+
return originalCleverTapID
131+
}
132+
133+
if shouldAppendUserId {
134+
return "\(ctDevice)_\(userIdHash)"
135+
}
136+
137+
return ctDevice
138+
}
92139

93140
var profile: [AnyHashable: Any] {
94141
[Constants.Identity: userId]
@@ -97,4 +144,8 @@ class IdentityManager {
97144
var isAnonymous: Bool {
98145
userId == deviceId
99146
}
147+
148+
var shouldAppendUserId: Bool {
149+
userIdHash != anonymousLoginUserId && userId != deviceId
150+
}
100151
}

LeanplumSDK/LeanplumSDK/ClassesSwift/Utilities/Utilities.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
// Copyright © 2021 Leanplum. All rights reserved.
77

88
import Foundation
9+
import CommonCrypto
910

10-
// TODO: Remove Utilities class when we add proper models
11-
public class Utilities: NSObject {
11+
public class Utilities: NSObject {
1212
/**
1313
* Returns Leanplum message Id from Notification userInfo.
1414
* Use this method to identify Leanplum Notifications
@@ -22,4 +22,37 @@ public class Utilities: NSObject {
2222
}
2323
return nil
2424
}
25+
26+
@objc public static func sha256(data: Data) -> Data {
27+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
28+
data.withUnsafeBytes {
29+
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
30+
}
31+
return Data(hash)
32+
}
33+
34+
@objc public static func sha256(string: String) -> String? {
35+
guard let messageData = string.data(using: String.Encoding.utf8) else { return nil }
36+
let hashedData = sha256(data: messageData)
37+
return hashedData.hexEncodedString()
38+
}
39+
40+
@objc public static func sha256_128(string: String) -> String? {
41+
guard let str = sha256(string: string) else { return nil }
42+
43+
let hexLength = 256/2/4
44+
return substring(string: str, openEndIndex: hexLength)
45+
}
46+
47+
@objc public static func sha256_40(string: String) -> String? {
48+
guard let str = sha256(string: string) else { return nil }
49+
50+
let hexLength = 40/4
51+
return substring(string: str, openEndIndex: hexLength)
52+
}
53+
54+
static func substring(string: String, openEndIndex: Int) -> String {
55+
let endIndex = string.index(string.startIndex, offsetBy: openEndIndex)
56+
return String(string[..<endIndex])
57+
}
2558
}

LeanplumSDKApp/LeanplumSDKApp.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
6A39C48F27283EE8000D5320 /* NotificationsProxyTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A39C48E27283EE8000D5320 /* NotificationsProxyTest.swift */; };
134134
6A54E9CD273B156600DE0E61 /* NotificationsManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A54E9CC273B156600DE0E61 /* NotificationsManagerTest.swift */; };
135135
6A81EDDE284681D8001B4BE9 /* TiedPrioritiesDelay.json in Resources */ = {isa = PBXBuildFile; fileRef = 6A81EDDD284681D8001B4BE9 /* TiedPrioritiesDelay.json */; };
136+
6A8411BB2903080A009F0431 /* UtilitiesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8411BA2903080A009F0431 /* UtilitiesTest.swift */; };
136137
6A9D0A9627342BA300466133 /* NotificationsProxyiOS9Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9D0A9527342BA300466133 /* NotificationsProxyiOS9Test.swift */; };
137138
6A9D0A9827342EE300466133 /* NotificationsManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9D0A9727342EE300466133 /* NotificationsManagerMock.swift */; };
138139
6A9D0A9A273430A700466133 /* NotificationTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9D0A99273430A700466133 /* NotificationTestHelper.swift */; };
@@ -335,6 +336,7 @@
335336
6A39C48E27283EE8000D5320 /* NotificationsProxyTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsProxyTest.swift; sourceTree = "<group>"; };
336337
6A54E9CC273B156600DE0E61 /* NotificationsManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManagerTest.swift; sourceTree = "<group>"; };
337338
6A81EDDD284681D8001B4BE9 /* TiedPrioritiesDelay.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TiedPrioritiesDelay.json; sourceTree = "<group>"; };
339+
6A8411BA2903080A009F0431 /* UtilitiesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilitiesTest.swift; sourceTree = "<group>"; };
338340
6A9D0A9527342BA300466133 /* NotificationsProxyiOS9Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsProxyiOS9Test.swift; sourceTree = "<group>"; };
339341
6A9D0A9727342EE300466133 /* NotificationsManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManagerMock.swift; sourceTree = "<group>"; };
340342
6A9D0A99273430A700466133 /* NotificationTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTestHelper.swift; sourceTree = "<group>"; };
@@ -487,6 +489,7 @@
487489
075AB1082684AAD9007CA1BD /* Utilities */,
488490
6A39C48D27283EE7000D5320 /* LeanplumSDKTests-Bridging-Header.h */,
489491
6A07FDAF283544E300995BE3 /* DictionaryValueKeyPathTest.swift */,
492+
6A8411BA2903080A009F0431 /* UtilitiesTest.swift */,
490493
);
491494
path = Classes;
492495
sourceTree = "<group>";
@@ -1018,6 +1021,7 @@
10181021
C9D503672754C9DC0034C5B3 /* PushNotificationSettingsTest.swift in Sources */,
10191022
075AB1262684AAD9007CA1BD /* LPRequest+Extension.m in Sources */,
10201023
6A9D0A9A273430A700466133 /* NotificationTestHelper.swift in Sources */,
1024+
6A8411BB2903080A009F0431 /* UtilitiesTest.swift in Sources */,
10211025
075AB12B2684AAD9007CA1BD /* LPOpenUrlMessageTemplateTest.m in Sources */,
10221026
075AB1132684AAD9007CA1BD /* LPEventDataManagerTest.m in Sources */,
10231027
075AB1122684AAD9007CA1BD /* LPUtilsTest.m in Sources */,

0 commit comments

Comments
 (0)