diff --git a/CleverTapSDK/CTLocalDataStore.m b/CleverTapSDK/CTLocalDataStore.m index b36044f19..57ef7deef 100644 --- a/CleverTapSDK/CTLocalDataStore.m +++ b/CleverTapSDK/CTLocalDataStore.m @@ -1019,6 +1019,41 @@ - (NSMutableDictionary *)decryptPIIDataIfEncrypted:(NSMutableDictionary *)profil return updatedProfile; } + else if (lastEncryptionLevel == CleverTapEncryptionHigh && self.config.cryptManager) { + // Always store the local profile data in decrypted values. + NSMutableDictionary *updatedProfile = [NSMutableDictionary new]; + + for (NSString *key in profile) { + @try { + // Validate the value before attempting to decrypt + id value = profile[key]; + if (!value || ![value isKindOfClass:[NSString class]]) { + CleverTapLogDebug(self.config.logLevel, @"%@: Invalid value for key: %@, skipping decryption", self, key); + updatedProfile[key] = value ?: [NSNull null]; + continue; + } + + NSString *stringValue = [NSString stringWithFormat:@"%@", value]; + NSString *decryptedString = [self.config.cryptManager decryptString:stringValue]; + + // Validate decryption result + if (!decryptedString) { + CleverTapLogDebug(self.config.logLevel, @"%@: Failed to decrypt data for key: %@", self, key); + // Return original value if decryption fails + updatedProfile[key] = stringValue; + } else { + updatedProfile[key] = decryptedString; + } + } @catch (NSException *e) { + CleverTapLogDebug(self.config.logLevel, @"%@: Exception during decryption for key %@: %@", self, key, e); + // Add original value to avoid data loss + updatedProfile[key] = profile[key]; + } + + } + + return updatedProfile; + } return profile; } @@ -1036,6 +1071,14 @@ - (NSMutableDictionary *)cryptValuesIfNeeded:(NSMutableDictionary *)profile { } return updatedProfile; } + else if (self.config.encryptionLevel == CleverTapEncryptionHigh && self.config.cryptManager) { + NSMutableDictionary *updatedProfile = [NSMutableDictionary new]; + for (NSString *key in profile) { + NSString *value = [NSString stringWithFormat:@"%@",profile[key]]; + updatedProfile[key] = [self.config.cryptManager encryptString:value]; + } + return updatedProfile; + } return profile; } diff --git a/CleverTapSDK/CTPlistInfo.m b/CleverTapSDK/CTPlistInfo.m index 82d673867..ac08ee5ba 100644 --- a/CleverTapSDK/CTPlistInfo.m +++ b/CleverTapSDK/CTPlistInfo.m @@ -137,9 +137,13 @@ - (void)setEncryption:(NSString *)encryptionLevel { _encryptionLevel = CleverTapEncryptionNone; } else if (encryptionLevel && [encryptionLevel isEqualToString:@"1"]) { _encryptionLevel = CleverTapEncryptionMedium; - } else { + } + else if (encryptionLevel && [encryptionLevel isEqualToString:@"2"]) { + _encryptionLevel = CleverTapEncryptionHigh; + } + else { _encryptionLevel = CleverTapEncryptionNone; - CleverTapLogStaticInternal(@"Supported encryption levels are only 0 and 1. Setting it to 0 by default"); + CleverTapLogStaticInternal(@"Supported encryption levels are only 0, 1 and 2. Setting it to 0 by default"); } } diff --git a/CleverTapSDK/CleverTap.h b/CleverTapSDK/CleverTap.h index c6c36670b..7051cc2ed 100644 --- a/CleverTapSDK/CleverTap.h +++ b/CleverTapSDK/CleverTap.h @@ -1,6 +1,7 @@ #import #import #import + #if !TARGET_OS_TV #import #endif @@ -61,11 +62,27 @@ typedef NS_ENUM(int, CTSignedCallEvent) { SIGNED_CALL_END_EVENT }; +/** + The encryption level used by CleverTap to secure stored data. + + Each level defines the extent of encryption applied to user and event data. + + - `CleverTapEncryptionNone` (0): No encryption. Data is stored as plain text. + - `CleverTapEncryptionMedium` (1): Encrypts Personally Identifiable Information (PII) only (e.g., Email, Name, Identitiy). + - `CleverTapEncryptionHigh` (2): Encrypts both PII and non-PII data. + */ typedef NS_ENUM(int, CleverTapEncryptionLevel) { + /// No encryption. Data is stored in plain text. CleverTapEncryptionNone = 0, - CleverTapEncryptionMedium = 1 + + /// Encrypts only PII (Personally Identifiable Information) data. + CleverTapEncryptionMedium = 1, + + /// Encrypts all data, including non-PII data. + CleverTapEncryptionHigh = 2, }; + typedef void (^CleverTapFetchInAppsBlock)(BOOL success); @interface CleverTap : NSObject diff --git a/CleverTapSDK/CleverTap.m b/CleverTapSDK/CleverTap.m index e86cf69e4..1190912c2 100644 --- a/CleverTapSDK/CleverTap.m +++ b/CleverTapSDK/CleverTap.m @@ -2034,21 +2034,60 @@ - (void)inflateQueuesAsync { } - (void)inflateEventsQueue { - self.eventsQueue = (NSMutableArray *)[CTPreferences unarchiveFromFile:[self eventsFileName] ofType:[NSMutableArray class] removeFile:YES]; - if (!self.eventsQueue || [self isMuted]) { + // If the previous encryption level was 2/high, decrypt the object + BOOL wasEncrypted = (self.config.cryptManager.previousEncryptionLevel == CleverTapEncryptionHigh); + + if (wasEncrypted) { + // File was encrypted, so decrypt when reading + self.eventsQueue = (NSMutableArray *)[self.config.cryptManager decryptObject: + [CTPreferences unarchiveFromFile:[self eventsFileName] + ofType:[NSMutableArray class] + removeFile:YES]]; + } else { + // File was stored raw + self.eventsQueue = (NSMutableArray *)[CTPreferences unarchiveFromFile: + [self eventsFileName] ofType:[NSMutableArray class] removeFile:YES]; + } + + // fallback incase decryption fails + if (!self.eventsQueue || ![self.eventsQueue isKindOfClass:[NSMutableArray class]] || [self isMuted]) { self.eventsQueue = [NSMutableArray array]; } } - (void)inflateProfileQueue { - self.profileQueue = (NSMutableArray *)[CTPreferences unarchiveFromFile:[self profileEventsFileName] ofType:[NSMutableArray class] removeFile:YES]; + // If the previous encryption level was 2/high, decrypt the object + BOOL wasEncrypted = (self.config.cryptManager.previousEncryptionLevel == CleverTapEncryptionHigh); + + if (wasEncrypted) { + // File was encrypted, so decrypt when reading + self.profileQueue = (NSMutableArray *)[self.config.cryptManager decryptObject: + [CTPreferences unarchiveFromFile:[self profileEventsFileName] + ofType:[NSMutableArray class] + removeFile:YES]]; + } else { + // File was stored raw + self.profileQueue = (NSMutableArray *)[CTPreferences unarchiveFromFile:[self profileEventsFileName] ofType:[NSMutableArray class] removeFile:YES]; + } if (!self.profileQueue || [self isMuted]) { self.profileQueue = [NSMutableArray array]; } } - (void)inflateNotificationsQueue { - self.notificationsQueue = (NSMutableArray *)[CTPreferences unarchiveFromFile:[self notificationsFileName] ofType:[NSMutableArray class] removeFile:YES]; + // If the previous encryption level was 2/high, decrypt the object + BOOL wasEncrypted = (self.config.cryptManager.previousEncryptionLevel == CleverTapEncryptionHigh); + + if (wasEncrypted) { + // File was encrypted, so decrypt when reading + self.notificationsQueue = (NSMutableArray *)[self.config.cryptManager decryptObject: + [CTPreferences unarchiveFromFile:[self notificationsFileName] + ofType:[NSMutableArray class] + removeFile:YES]]; + } else { + // File was stored raw + self.notificationsQueue = (NSMutableArray *)[CTPreferences unarchiveFromFile:[self notificationsFileName] ofType:[NSMutableArray class] removeFile:YES]; + } if (!self.notificationsQueue || [self isMuted]) { self.notificationsQueue = [NSMutableArray array]; } @@ -2079,6 +2118,7 @@ - (void)persistOrClearQueues { if ([self isMuted]) { [self clearQueues]; } else { + // encrypt if level has been changed to 2/high [self persistProfileQueue]; [self persistEventsQueue]; [self persistNotificationsQueue]; @@ -2087,27 +2127,36 @@ - (void)persistOrClearQueues { - (void)persistEventsQueue { NSString *fileName = [self eventsFileName]; - NSMutableArray *eventsCopy; + id eventsCopy; @synchronized (self) { eventsCopy = [NSMutableArray arrayWithArray:[self.eventsQueue copy]]; + if (self.config.encryptionLevel == CleverTapEncryptionHigh) { + eventsCopy = [self.config.cryptManager encryptObject:eventsCopy]; + } } [CTPreferences archiveObject:eventsCopy forFileName:fileName config:_config]; } - (void)persistProfileQueue { NSString *fileName = [self profileEventsFileName]; - NSMutableArray *profileEventsCopy; + id profileEventsCopy; @synchronized (self) { profileEventsCopy = [NSMutableArray arrayWithArray:[self.profileQueue copy]]; + if (self.config.encryptionLevel == CleverTapEncryptionHigh) { + profileEventsCopy = [self.config.cryptManager encryptObject:profileEventsCopy]; + } } [CTPreferences archiveObject:profileEventsCopy forFileName:fileName config:_config]; } - (void)persistNotificationsQueue { NSString *fileName = [self notificationsFileName]; - NSMutableArray *notificationsCopy; + id notificationsCopy; @synchronized (self) { notificationsCopy = [NSMutableArray arrayWithArray:[self.notificationsQueue copy]]; + if (self.config.encryptionLevel == CleverTapEncryptionHigh) { + notificationsCopy = [self.config.cryptManager encryptObject:notificationsCopy]; + } } [CTPreferences archiveObject:notificationsCopy forFileName:fileName config:_config]; } @@ -3659,7 +3708,7 @@ - (void)initializeInboxWithCallback:(CleverTapInboxSuccessBlock)callback { return; } if (self.deviceInfo.deviceId) { - self.inboxController = [[CTInboxController alloc] initWithAccountId: [self.config.accountId copy] guid: [self.deviceInfo.deviceId copy]]; + self.inboxController = [[CTInboxController alloc] initWithAccountId: [self.config.accountId copy] guid: [self.deviceInfo.deviceId copy] encryptionLevel:self.config.encryptionLevel previousEncryptionLevel:self.config.cryptManager.previousEncryptionLevel encryptionManager:self.config.cryptManager]; self.inboxController.delegate = self; [CTUtils runSyncMainQueue: ^{ callback(self.inboxController.isInitialized); @@ -3814,7 +3863,7 @@ - (void)dismissAppInbox { - (void)_resetInbox { if (self.inboxController && self.inboxController.isInitialized && self.deviceInfo.deviceId) { - self.inboxController = [[CTInboxController alloc] initWithAccountId: [self.config.accountId copy] guid: [self.deviceInfo.deviceId copy]]; + self.inboxController = [[CTInboxController alloc] initWithAccountId: [self.config.accountId copy] guid: [self.deviceInfo.deviceId copy] encryptionLevel:self.config.encryptionLevel previousEncryptionLevel:self.config.cryptManager.previousEncryptionLevel encryptionManager:self.config.cryptManager]; self.inboxController.delegate = self; } } diff --git a/CleverTapSDK/Encryption/CTEncryptionManager.h b/CleverTapSDK/Encryption/CTEncryptionManager.h index ba9a5c2d9..c78ef706a 100644 --- a/CleverTapSDK/Encryption/CTEncryptionManager.h +++ b/CleverTapSDK/Encryption/CTEncryptionManager.h @@ -29,6 +29,8 @@ NS_ASSUME_NONNULL_BEGIN */ @interface CTEncryptionManager : NSObject +@property (nonatomic, assign) CleverTapEncryptionLevel previousEncryptionLevel; + /** * Initializes the encryption manager with an account ID. * diff --git a/CleverTapSDK/Encryption/CTEncryptionManager.m b/CleverTapSDK/Encryption/CTEncryptionManager.m index 33bef8589..2f682ff9b 100644 --- a/CleverTapSDK/Encryption/CTEncryptionManager.m +++ b/CleverTapSDK/Encryption/CTEncryptionManager.m @@ -84,6 +84,8 @@ - (void)updateEncryptionLevel:(CleverTapEncryptionLevel)encryptionLevel { _encryptionLevel = encryptionLevel; NSString *encryptionKey = [CTUtils getKeyWithSuffix:kENCRYPTION_KEY accountID:_accountID]; long lastEncryptionLevel = [CTPreferences getIntForKey:encryptionKey withResetValue:0]; + self.previousEncryptionLevel = (CleverTapEncryptionLevel)lastEncryptionLevel; + if (lastEncryptionLevel != _encryptionLevel) { CleverTapLogStaticInternal(@"CleverTap Encryption level changed for account: %@ to: %d", _accountID, _encryptionLevel); [self updateCachedGUIDS]; @@ -341,7 +343,7 @@ - (void)updateCachedGUIDS { NSString *key = components[0]; NSString *identifier = components[1]; - NSString *processedIdentifier = self->_encryptionLevel == CleverTapEncryptionMedium ? + NSString *processedIdentifier = (self->_encryptionLevel == CleverTapEncryptionMedium || self->_encryptionLevel == CleverTapEncryptionHigh) ? [self encryptString:identifier] : [self decryptString:identifier]; if (processedIdentifier) { diff --git a/CleverTapSDK/Inbox/controllers/CTInboxController.h b/CleverTapSDK/Inbox/controllers/CTInboxController.h index c4d8a8327..48dcafd29 100755 --- a/CleverTapSDK/Inbox/controllers/CTInboxController.h +++ b/CleverTapSDK/Inbox/controllers/CTInboxController.h @@ -1,4 +1,6 @@ #import +#import "CleverTap.h" +#import "CTEncryptionManager.h" @protocol CTInboxDelegate @required @@ -22,7 +24,10 @@ NS_ASSUME_NONNULL_BEGIN // blocking, call off main thread - (instancetype _Nullable)initWithAccountId:(NSString *)accountId - guid:(NSString *)guid; + guid:(NSString *)guid + encryptionLevel:(CleverTapEncryptionLevel)encryptionLevel + previousEncryptionLevel:(CleverTapEncryptionLevel)previousEncryptionLevel + encryptionManager:(CTEncryptionManager*)encryptionManager; - (void)updateMessages:(NSArray *)messages; - (NSDictionary * _Nullable )messageForId:(NSString *)messageId; diff --git a/CleverTapSDK/Inbox/controllers/CTInboxController.m b/CleverTapSDK/Inbox/controllers/CTInboxController.m index c7d35572e..1ea108b6a 100755 --- a/CleverTapSDK/Inbox/controllers/CTInboxController.m +++ b/CleverTapSDK/Inbox/controllers/CTInboxController.m @@ -16,6 +16,9 @@ @interface CTInboxController () @property (nonatomic, copy, readonly) NSString *accountId; @property (nonatomic, copy, readonly) NSString *guid; @property (nonatomic, copy, readonly) NSString *userIdentifier; +@property (nonatomic, assign) CleverTapEncryptionLevel encryptionLevel; +@property (nonatomic, assign) CleverTapEncryptionLevel previousEncryptionLevel; +@property (nonatomic, strong) CTEncryptionManager *encryptionManager; @property (nonatomic, strong, readonly) CTUserMO *user; // Instance-specific context @@ -33,7 +36,11 @@ @implementation CTInboxController #pragma mark - Initialization // blocking, call off main thread -- (instancetype)initWithAccountId:(NSString *)accountId guid:(NSString *)guid { +- (instancetype)initWithAccountId:(NSString *)accountId + guid:(NSString *)guid + encryptionLevel:(CleverTapEncryptionLevel)encryptionLevel + previousEncryptionLevel:(CleverTapEncryptionLevel)previousEncryptionLevel + encryptionManager:(nonnull CTEncryptionManager *)encryptionManager { if (self = [super init]) { // Initialize shared coordinator if needed @@ -44,6 +51,9 @@ - (instancetype)initWithAccountId:(NSString *)accountId guid:(NSString *)guid { if (_isInitialized) { _accountId = [accountId copy]; _guid = [guid copy]; + _encryptionLevel = encryptionLevel; + _previousEncryptionLevel = previousEncryptionLevel; + _encryptionManager = encryptionManager; NSString *userIdentifier = [NSString stringWithFormat:@"%@:%@", accountId, guid]; _userIdentifier = userIdentifier; @@ -71,7 +81,7 @@ - (instancetype)initWithAccountId:(NSString *)accountId guid:(NSString *)guid { @"accountId": accountId, @"guid": guid, @"identifier": userIdentifier - } forContext:strongSelf.context]; + } forContext:strongSelf.context encryptionManager:encryptionManager]; [strongSelf _save]; }]; @@ -146,7 +156,11 @@ - (void)updateMessages:(NSArray *)messages { if (!strongSelf) return; CleverTapLogStaticInternal(@"%@: updating messages: %@", strongSelf.user, messages); - BOOL haveUpdates = [strongSelf.user updateMessages:messages + + // Pre-process messages for encryption if needed + NSArray *processedMessages = [strongSelf processMessagesForEncryption:messages]; + + BOOL haveUpdates = [strongSelf.user updateMessages:processedMessages forContext:strongSelf.context]; if (haveUpdates) { @@ -156,6 +170,38 @@ - (void)updateMessages:(NSArray *)messages { }]; } +- (NSArray *)processMessagesForEncryption:(NSArray *)messages { + // If not CleverTapEncryptionHigh, return messages unchanged + if (self.encryptionLevel != CleverTapEncryptionHigh || !self.encryptionManager) { + return messages; + } + + NSMutableArray *processedMessages = [NSMutableArray arrayWithCapacity:messages.count]; + + for (NSDictionary *message in messages) { + // Encrypt the message dictionary and create a wrapper + NSString *encryptedJSON = [self.encryptionManager encryptObject:message]; + if (encryptedJSON) { + // Create a new message dictionary with encrypted JSON + NSMutableDictionary *processedMessage = [message mutableCopy]; + + // Replace the original message data with encrypted version + // but keep the _id and other lookup fields unencrypted for CoreData queries + processedMessage[@"_ct_encrypted_payload"] = encryptedJSON; + processedMessage[@"_ct_is_encrypted"] = @YES; + + CleverTapLogStaticInternal(@"Pre-encrypted message for ID: %@", message[@"_id"]); + [processedMessages addObject:processedMessage]; + } else { + // If encryption fails, store unencrypted + CleverTapLogStaticDebug(@"Failed to encrypt message ID: %@, storing unencrypted", message[@"_id"]); + [processedMessages addObject:message]; + } + } + + return [processedMessages copy]; +} + - (void)deleteMessageWithId:(NSString *)messageId { if (!self.isInitialized || !messageId) return; @@ -368,6 +414,15 @@ - (void)_deleteMessages:(NSArray*)messages { // Always call from inside context performBlock - (BOOL)_save { + // Handle encryption level transitions for existing messages + if (self.encryptionLevel != self.previousEncryptionLevel && [self.user.messages count] > 0) { + [self migrateMessagesEncryption]; + + // Update previousEncryptionLevel to prevent repeated migrations + self.previousEncryptionLevel = self.encryptionLevel; + CleverTapLogStaticDebug(@"Migration completed, updated previousEncryptionLevel to %d", (int)self.encryptionLevel); + } + if (!self.context.hasChanges) { return YES; } @@ -383,6 +438,56 @@ - (BOOL)_save { return success; } +- (void)migrateMessagesEncryption { + CleverTapLogStaticDebug(@"Migrating inbox messages encryption from level %d to %d", + (int)self.previousEncryptionLevel, (int)self.encryptionLevel); + + for (CTMessageMO *msg in self.user.messages) { + [self migrateMessageEncryption:msg + fromLevel:self.previousEncryptionLevel + toLevel:self.encryptionLevel]; + } +} + +- (void)migrateMessageEncryption:(CTMessageMO *)msg + fromLevel:(CleverTapEncryptionLevel)fromLevel + toLevel:(CleverTapEncryptionLevel)toLevel { + + // Scenario 1: None/Medium → High (need to encrypt json) + if ((fromLevel == CleverTapEncryptionNone || fromLevel == CleverTapEncryptionMedium) && + toLevel == CleverTapEncryptionHigh) { + + if (msg.json && ![self isJSONPropertyEncrypted:msg.json]) { + NSString *encryptedJSON = [self.encryptionManager encryptObject:msg.json]; + if (encryptedJSON) { + msg.json = encryptedJSON; + CleverTapLogStaticDebug(@"Encrypted inbox message json for message ID: %@", msg.id); + } + } + } + + // Scenario 2: High → None/Medium (need to decrypt json) + else if (fromLevel == CleverTapEncryptionHigh && + (toLevel == CleverTapEncryptionNone || toLevel == CleverTapEncryptionMedium)) { + + if (msg.json && [self isJSONPropertyEncrypted:msg.json]) { + id decryptedJSON = [self.encryptionManager decryptObject:(NSString *)msg.json]; + if (decryptedJSON) { + msg.json = decryptedJSON; + CleverTapLogStaticDebug(@"Decrypted inbox message json for message ID: %@", msg.id); + } + } + } +} + +- (BOOL)isJSONPropertyEncrypted:(id)jsonProperty { + if ([jsonProperty isKindOfClass:[NSString class]]) { + NSString *jsonString = (NSString *)jsonProperty; + return [self.encryptionManager isTextAESGCMEncrypted:jsonString]; + } + return NO; +} + #pragma mark - Delegate Notification - (void)_notifyUpdate { diff --git a/CleverTapSDK/Inbox/models/CTMessageMO+CoreDataProperties.m b/CleverTapSDK/Inbox/models/CTMessageMO+CoreDataProperties.m index 88eb9b944..08f4ae0ce 100755 --- a/CleverTapSDK/Inbox/models/CTMessageMO+CoreDataProperties.m +++ b/CleverTapSDK/Inbox/models/CTMessageMO+CoreDataProperties.m @@ -1,5 +1,6 @@ #import "CTMessageMO.h" #import "CTConstants.h" +#import "CTUserMO.h" @implementation CTMessageMO (CoreDataProperties) @@ -13,8 +14,21 @@ - (instancetype)initWithJSON:(NSDictionary *)json forContext:(NSManagedObjectCon self = [self initWithEntity:[NSEntityDescription entityForName:@"CTMessage" inManagedObjectContext:context] insertIntoManagedObjectContext:context]; if (self != nil) { + // Check if this message was pre-encrypted + if (json[@"_ct_is_encrypted"] && [json[@"_ct_is_encrypted"] boolValue]) { + // Use the encrypted payload as the json property + if (!json[@"_ct_encrypted_payload"]) { + CleverTapLogStaticDebug(@"Message marked as encrypted but missing _ct_encrypted_payload"); + self.json = [json copy]; + } + else { + self.json = json[@"_ct_encrypted_payload"]; + } + } else { + // Use the original message + self.json = [json copy]; + } - self.json = [json copy]; self.tags = json[@"msg"][@"tags"]; NSString *id = json[@"_id"]; @@ -37,7 +51,28 @@ - (instancetype)initWithJSON:(NSDictionary *)json forContext:(NSManagedObjectCon } - (NSDictionary *)toJSON { - NSMutableDictionary *json = [NSMutableDictionary dictionaryWithDictionary:self.json]; + id jsonData = self.json; + + // If json is encrypted (stored as string), decrypt it first + if ([jsonData isKindOfClass:[NSString class]]) { + // Get encryption manager from context + CTEncryptionManager *encryptionManager = self.user.encryptionManager; + NSString *encryptedString = (NSString *)jsonData; + + // Check if it's actually encrypted using AES-GCM markers + if (encryptionManager && [encryptionManager isTextAESGCMEncrypted:encryptedString]) { + id decryptedObj = [encryptionManager decryptObject:encryptedString]; + if (decryptedObj && [decryptedObj isKindOfClass:[NSDictionary class]]) { + jsonData = decryptedObj; + } + else { + CleverTapLogStaticDebug(@"Failed to decrypt message with ID: %@, returning empty dictionary", self.id); + return @{@"isRead": @(self.isRead), @"date": @(self.date)}; + } + } + } + + NSMutableDictionary *json = [NSMutableDictionary dictionaryWithDictionary:jsonData]; json[@"isRead"] = @(self.isRead); json[@"date"] = @(self.date); return json; diff --git a/CleverTapSDK/Inbox/models/CTUserMO+CoreDataProperties.h b/CleverTapSDK/Inbox/models/CTUserMO+CoreDataProperties.h index b97b1a6a7..3d5fd7575 100755 --- a/CleverTapSDK/Inbox/models/CTUserMO+CoreDataProperties.h +++ b/CleverTapSDK/Inbox/models/CTUserMO+CoreDataProperties.h @@ -6,7 +6,7 @@ NS_ASSUME_NONNULL_BEGIN @interface CTUserMO (CoreDataProperties) -+ (instancetype _Nullable)fetchOrCreateFromJSON:(NSDictionary *)json forContext:(NSManagedObjectContext *)context; ++ (instancetype _Nullable)fetchOrCreateFromJSON:(NSDictionary *)json forContext:(NSManagedObjectContext *)context encryptionManager:(nonnull CTEncryptionManager *)encryptionManager; - (BOOL)updateMessages:(NSArray *)messages forContext:(NSManagedObjectContext *)context; @property (nullable, nonatomic, copy) NSString *accountId; diff --git a/CleverTapSDK/Inbox/models/CTUserMO+CoreDataProperties.m b/CleverTapSDK/Inbox/models/CTUserMO+CoreDataProperties.m index ee0accc52..adcc928ad 100755 --- a/CleverTapSDK/Inbox/models/CTUserMO+CoreDataProperties.m +++ b/CleverTapSDK/Inbox/models/CTUserMO+CoreDataProperties.m @@ -17,7 +17,7 @@ - (void)removeMessages:(NSOrderedSet *)values; @implementation CTUserMO (CoreDataProperties) -+ (instancetype _Nullable)fetchOrCreateFromJSON:(NSDictionary *)json forContext:(NSManagedObjectContext *)context { ++ (instancetype _Nullable)fetchOrCreateFromJSON:(NSDictionary *)json forContext:(NSManagedObjectContext *)context encryptionManager:(nonnull CTEncryptionManager *)encryptionManager { CTUserMO *_user; @try { NSString *identifier = json[@"identifier"]; @@ -37,9 +37,10 @@ + (instancetype _Nullable)fetchOrCreateFromJSON:(NSDictionary *)json forContext: } if ([results count] > 0) { _user = results[0]; + _user.encryptionManager = encryptionManager; CleverTapLogStaticInternal(@"Found existing %@", _user); } else { - _user = [[CTUserMO alloc] initWithJSON:json forContext:context]; + _user = [[CTUserMO alloc] initWithJSON:json forContext:context encryptionManager:encryptionManager]; } } @catch (NSException *e) { @@ -49,7 +50,7 @@ + (instancetype _Nullable)fetchOrCreateFromJSON:(NSDictionary *)json forContext: return _user; } -- (instancetype)initWithJSON:(NSDictionary *)json forContext:(NSManagedObjectContext *)context { +- (instancetype)initWithJSON:(NSDictionary *)json forContext:(NSManagedObjectContext *)context encryptionManager:(nonnull CTEncryptionManager *)encryptionManager { CleverTapLogStaticInternal(@"Initializing new CTUserMO with data: %@", json); self = [self initWithEntity:[NSEntityDescription entityForName:@"CTUser" inManagedObjectContext:context] insertIntoManagedObjectContext:context]; NSString *accountId = json[@"accountId"]; @@ -64,6 +65,8 @@ - (instancetype)initWithJSON:(NSDictionary *)json forContext:(NSManagedObjectCon if (identifier) { self.identifier = identifier; } + + self.encryptionManager = encryptionManager; return self; } diff --git a/CleverTapSDK/Inbox/models/CTUserMO.h b/CleverTapSDK/Inbox/models/CTUserMO.h index fd98fc866..75c15f3e1 100755 --- a/CleverTapSDK/Inbox/models/CTUserMO.h +++ b/CleverTapSDK/Inbox/models/CTUserMO.h @@ -1,8 +1,9 @@ #import #import +#import "CTEncryptionManager.h" @interface CTUserMO : NSManagedObject - +@property (nonatomic, strong) CTEncryptionManager *encryptionManager; @end #import "CTUserMO+CoreDataProperties.h" diff --git a/CleverTapSDK/Inbox/models/CTUserMO.m b/CleverTapSDK/Inbox/models/CTUserMO.m index 42a0d566b..61e0c4346 100755 --- a/CleverTapSDK/Inbox/models/CTUserMO.m +++ b/CleverTapSDK/Inbox/models/CTUserMO.m @@ -1,6 +1,7 @@ #import "CTUserMO.h" @implementation CTUserMO +@synthesize encryptionManager; - (NSString*)description { return [NSString stringWithFormat:@"CTUserMO: %@ messages count=%lu", self.identifier, (long)[self.messages count]]; diff --git a/CleverTapSDK/ProductExperiences/CTVarCache.m b/CleverTapSDK/ProductExperiences/CTVarCache.m index d0f96d2d6..f7fff34a2 100644 --- a/CleverTapSDK/ProductExperiences/CTVarCache.m +++ b/CleverTapSDK/ProductExperiences/CTVarCache.m @@ -3,6 +3,7 @@ #import "CTConstants.h" #import "CTPreferences.h" #import "ContentMerger.h" +#import "CTEncryptionManager.h" @interface CTVarCache() @property (strong, nonatomic) NSMutableDictionary *valuesFromClient; @@ -14,6 +15,8 @@ @interface CTVarCache() @property (nonatomic, strong) CTDeviceInfo *deviceInfo; @property (nonatomic, strong) CTFileDownloader *fileDownloader; @property (strong, nonatomic) NSMutableDictionary *fileVarsInDownload; +@property (nonatomic, assign) CleverTapEncryptionLevel encryptionLevel; +@property (nonatomic, assign) CleverTapEncryptionLevel previousEncryptionLevel; @end @implementation CTVarCache @@ -25,6 +28,8 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)config self.config = config; self.deviceInfo = deviceInfo; self.fileDownloader = fileDownloader; + self.encryptionLevel = config.encryptionLevel; + self.previousEncryptionLevel = config.cryptManager.previousEncryptionLevel; [self initialize]; } return self; @@ -192,7 +197,10 @@ - (void)loadDiffs { unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:diffsData]; #pragma clang diagnostic pop } - NSDictionary *diffs = (NSDictionary *) [unarchiver decodeObjectForKey:CLEVERTAP_DEFAULTS_VARIABLES_KEY]; + NSDictionary *loadedDiffs = (NSDictionary *) [unarchiver decodeObjectForKey:CLEVERTAP_DEFAULTS_VARIABLES_KEY]; + + // Decrypt diffs if they were encrypted + NSDictionary *diffs = [self decryptDiffsIfNeeded:loadedDiffs]; [self applyVariableDiffs:diffs]; } @catch (NSException *exception) { @@ -201,22 +209,32 @@ - (void)loadDiffs { } - (void)saveDiffs { + // Handle encryption level migration if needed + if (self.encryptionLevel != self.previousEncryptionLevel) { + [self migrateVariablesEncryption]; + self.previousEncryptionLevel = self.encryptionLevel; // Update to prevent repeated migration + } + // Stores the variables on the device in case we don't have a connection. // Restores next time when the app is opened. // Diffs need to be locked incase other thread changes the diffs @synchronized (self.diffs) { + // Encrypt diffs if needed before saving + NSDictionary *diffsToSave = [self encryptDiffsIfNeeded:self.diffs]; + NSMutableData *diffsData = [[NSMutableData alloc] init]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:diffsData]; #pragma clang diagnostic pop - [archiver encodeObject:self.diffs forKey:CLEVERTAP_DEFAULTS_VARIABLES_KEY]; + [archiver encodeObject:diffsToSave forKey:CLEVERTAP_DEFAULTS_VARIABLES_KEY]; [archiver finishEncoding]; NSError *writeError = nil; NSString *fileName = [self dataArchiveFileName]; NSString *filePath = [CTPreferences filePathfromFileName:fileName]; - NSDataWritingOptions fileProtectionOption = _config.enableFileProtection ? NSDataWritingFileProtectionComplete : NSDataWritingAtomic; + NSDataWritingOptions fileProtectionOption = _config.enableFileProtection ? + NSDataWritingFileProtectionComplete : NSDataWritingAtomic; [diffsData writeToFile:filePath options:fileProtectionOption error:&writeError]; if (writeError) { CleverTapLogStaticInternal(@"%@ failed to write data at %@: %@", self, filePath, writeError); @@ -373,4 +391,81 @@ - (void)fileVarUpdated:(CTVar *)fileVar { } } +- (NSDictionary *)encryptDiffsIfNeeded:(NSDictionary *)diffs { + if (self.config.encryptionLevel != CleverTapEncryptionHigh || !self.config.cryptManager) { + return diffs; + } + + if (!diffs || [diffs count] == 0) { + return diffs; + } + + // Encrypt the entire diffs dictionary + NSString *encryptedDiffs = [self.config.cryptManager encryptObject:diffs]; + if (encryptedDiffs) { + CleverTapLogDebug(self.config.logLevel, @"%@: Encrypted variable diffs for storage", self); + return @{@"_ct_encrypted_vars": encryptedDiffs}; + } + + return diffs; +} + +- (NSDictionary *)decryptDiffsIfNeeded:(NSDictionary *)diffs { + if (!diffs || [diffs count] == 0) { + return diffs; + } + + // Check if this is encrypted data + if (diffs[@"_ct_encrypted_vars"]) { + NSString *encryptedString = diffs[@"_ct_encrypted_vars"]; + if ([self.config.cryptManager isTextAESGCMEncrypted:encryptedString]) { + id decryptedDiffs = [self.config.cryptManager decryptObject:encryptedString]; + if (decryptedDiffs && [decryptedDiffs isKindOfClass:[NSDictionary class]]) { + CleverTapLogDebug(self.config.logLevel, @"%@: Decrypted variable diffs from storage", self); + return decryptedDiffs; + } + } + } + + return diffs; +} + +- (void)migrateVariablesEncryption { + if (!self.diffs || [self.diffs count] == 0) { + return; + } + + CleverTapLogStaticInternal(@"Migrating variables encryption from level %d to %d", + (int)self.previousEncryptionLevel, (int)self.encryptionLevel); + + // Create mutable copy for migration + NSMutableDictionary *migratedDiffs = [self.diffs mutableCopy]; + + // Scenario 1: None/Medium → High (encrypt) + if ((self.previousEncryptionLevel == CleverTapEncryptionNone || + self.previousEncryptionLevel == CleverTapEncryptionMedium) && + self.encryptionLevel == CleverTapEncryptionHigh) { + + // If diffs are not encrypted, they will be encrypted in saveDiffs + CleverTapLogStaticInternal(@"Variables will be encrypted on next save"); + } + + // Scenario 2: High → None/Medium (decrypt) + else if (self.previousEncryptionLevel == CleverTapEncryptionHigh && + (self.encryptionLevel == CleverTapEncryptionNone || + self.encryptionLevel == CleverTapEncryptionMedium)) { + + // If diffs are encrypted, decrypt them + if (migratedDiffs[@"_ct_encrypted_vars"]) { + NSDictionary *decryptedDiffs = [self decryptDiffsIfNeeded:migratedDiffs]; + if (decryptedDiffs && decryptedDiffs != migratedDiffs) { + self.diffs = decryptedDiffs; + CleverTapLogStaticInternal(@"Decrypted variables for level change"); + } + } + } +} + + + @end