NSuserdefaults 是一种IOS常用的数据持久化的方式,操作简便,配合NSCoding 和NSKeyedArchiver,很容易将数据model转化成NSData直接存储在NSuserdefaults。那使用NSuserdefaults保存数据,数据的安全性如何呢?
NSuserdefaults 的本质是使用了plist存储数据,将存储在NSuserdefaults中的数据写入了一个以Bundle Identifier命名的plist中。
下面是一个简单的例子。
@interface TestInfo : NSObject<NSCoding>
@property (nonatomic,retain) NSString *username;
@property (nonatomic,retain) NSString *phone;
@property (nonatomic,retain) NSString *ticket;
@property (nonatomic,retain) NSString *email;
@property (nonatomic,retain) NSString *passport;
//混淆
@property (nonatomic,retain) NSString *uiofdsaouiSHJ;
@property (nonatomic,assign) BOOL isLGBT;
@property (nonatomic,retain) NSNumber *age;
-(void)saveLoginInfo;
@end
@implementation TestInfo
-(id)initWithCoder:(NSCoder *)aDecoder{
if (self=[self init]) {
self.username=[aDecoder decodeObjectForKey:@"username"];
self.uiofdsaouiSHJ=[aDecoder decodeObjectForKey:@"uiofdsaouiSHJ"];
self.phone = [aDecoder decodeObjectForKey:@"phone"];
self.email = [aDecoder decodeObjectForKey:@"email"];
self.passport = [aDecoder decodeObjectForKey:@"passport"];
self.ticket = [aDecoder decodeObjectForKey:@"ticket"];
self.isLGBT = [[aDecoder decodeObjectForKey:@"isLGBT"] boolValue];
self.age = [aDecoder decodeObjectForKey:@"ticket"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:_username forKey:@"username"];
[aCoder encodeObject:_uiofdsaouiSHJ forKey:@"uiofdsaouiSHJ"];
[aCoder encodeObject:_phone forKey:@"phone"];
[aCoder encodeObject:_email forKey:@"email"];
[aCoder encodeObject:_passport forKey:@"passport"];
[aCoder encodeObject:_ticket forKey:@"ticket"];
[aCoder encodeObject:[NSNumber numberWithBool:_isLGBT] forKey:@"isLGBT"];
[aCoder encodeObject:_age forKey:@"age"];
}
-(void)saveLoginInfo{
NSUserDefaults *cache=[NSUserDefaults standardUserDefaults];
NSString *gameKeyStr = [NSString stringWithFormat:@"CC_userinfos"];
NSData *logininfo=[NSKeyedArchiver archivedDataWithRootObject:self];
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:logininfo];
[cache setObject:array forKey:gameKeyStr];
}
@end
当打开plist时,可以看到存储下来的数据按照 NSuserdefaults 写入的数据类型被保存下来。
其中在例子代码中使用saveLoginInfo保存的数据,如下所示。是一串在plist保存的二进制数据,那么此段数据是否很容易被破解呢?
<62706c69 73743030 d4010203 04050623 24582476 65727369 6f6e5824
6f626a65 63747359 24617263 68697665 72542474 6f701200 0186a0ab
07080f0d 0910110a 1b1c1d55 246e756c 6cd9090a 0b0c0d0e 0f101112
13141516 1718191a 5570686f 6e655674 69636b65 74566973 4c474254
5624636c 6173735d 75696f66 6473616f 75695348 4a536167 65587573
65726e61 6d655565 6d61696c 58706173 73706f72 74800480 07800880
0a800380 09800280 05800609 1017d21e 1f20215a 24636c61 73736e61
6d655824 636c6173 73657358 54657374 496e666f a2202258 4e534f62
6a656374 5f100f4e 534b6579 65644172 63686976 6572d125 2654726f
6f748001 00080011 001a0023 002d0032 00370043 0049005c 00620069
00700077 00850089 00920098 00a100a3 00a500a7 00a900ab 00ad00af
00b100b3 00b400b6 00bb00c6 00cf00d8 00db00e4 00f600f9 00fe0000
00000000 02010000 00000000 00270000 00000000 00000000 00000000 0100>
按照encodeWithCoder来说,NSObject使用了NSCoder进行了encode。按照道理讲,在不知道类参数的情况下,是无法从这一段二进制数据中恢复出原来被加密的数据。
但是实际操作会是如何呢?假设在只拿到这个plist文件,不知道TestInfo类情况下。进行逐步分析。
为了简便,把这段二进制数据直接写入到demo的info.plist用作分析。
NSString *infoPath = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];
NSMutableDictionary *data = [[NSMutableDictionary alloc] initWithContentsOfFile:infoPath];
NSArray *cachearray=[data objectForKey:@"CC_userinfos"];
if (cachearray) {
for (int i=0; i<cachearray.count; i++) {
NSData *data=[cachearray objectAtIndex:i];
NSString *logininfo=[NSKeyedUnarchiver unarchiveObjectWithData:data];
}
}
对该段代码做一下说明,因为我不知道数据从什么类型转化来的,就先用NSString作为数据类型试试看,会有什么错误吧。
NSString *logininfo=[NSKeyedUnarchiver unarchiveObjectWithData:data];
不出意外,因为类型不匹配,Xcode报错,但是从出错信息中,确可以看出一些有趣的信息。
2016-05-05 20:02:03.260 NSUserDefaultsDataSaveDemo[48761:1143922] *** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (TestInfo) for key (root); the class may be defined in source code or a library that is not linked'
可以看到,报错信息中提示,这段数据原来的类是叫做“TestInfo“,但是我们还不知道这个类的组成是怎么样,那如果我把这段数据编码为NSString会怎么样?会不会从中再发现一点有趣的信息?
NSString *str =[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
NSLog(@"%@",str);
查看这个string的打印,发现更多令人兴奋的信息。
2016-05-05 20:11:05.130 NSUserDefaultsDataSaveDemo[48898:1151203] bplist00Ô&'T$topX$objectsX$versionY$archiverÑTroot€« U$nullÙ XpassportVticketUphoneSage]uiofdsaouiSHJV$classUemailVisLGBTXusername
因为理性的coder都会为自己的类定义可读性很高命名 这也算是一个编程规范,所以在这段NSSting中大概可以猜出TestInfo的若干属性(大写字母分隔),如passport、ticket、phone、age、email、username。
[uiofdsaouiSHJ、isLGBT]是我故意写的两个命名不规范的属性。
大概猜出属性的名字后,重新initWithCoder方法,便得到TestInfo的明文。
因为对NSuserdefaults保存数据有疑虑,做了以上的试验。证明了NSuserdefaults数据保存安全性差,在使用encodeObject之前,需要使用自己的加密算法进行加密,即使被分析出类后,也不至于读出明文数据。
但是该试验能够成功的基础是
1.Xcode强大的错误分析能力;
2.coder需要遵守的一些编码规范;
所以在以后的编码中,一是要注重自己的编码风格,规范化,二是任何数据存储都要考虑下是否安全,对于不熟悉的,要深挖一下;三是要正视编译器的报错信息,不要觉得有错是件坏事情,错误里边包含的信息量很大。
最后附上本文中使用到的例子。包含了encode部分、数据model和分析的代码。以上纯属黑科技,尤其是类属性参数分析那块依靠自己的猜测。但是结论还是蛮有意义。
例子代码