一、主流保證數(shù)據(jù)安全的方式
1、網(wǎng)絡(luò)傳輸安全
1.采用HTTPS通信協(xié)議
可防止抓包竊取、篡改傳輸數(shù)據(jù),大大增加了中間人攻擊的成本。
2.對敏感數(shù)據(jù)進(jìn)行簽名校正
采用非對稱加密的方式,對敏感數(shù)據(jù)使用密鑰加密,到了客戶端用公鑰解密,驗(yàn)證數(shù)據(jù)一致性,防止通信過程中被篡改。
3.采用密文傳輸
別人即使截取了傳輸信息,也無法看懂其中的意思。
2、本地存儲數(shù)據(jù)安全
1.數(shù)據(jù)庫加密
通過越獄設(shè)備可以很容易地把整個(gè)應(yīng)用包(包括里面的數(shù)據(jù))copy出來,這樣就可以獲取里面的數(shù)據(jù)庫,對于沒有加密的數(shù)據(jù)庫就可以非常輕易地讀取里面的信息,造成信息的泄漏。
數(shù)據(jù)庫加密可分兩個(gè)維度:1.整個(gè)數(shù)據(jù)庫加密;2.對部分字段先加密再存數(shù)據(jù)庫。
對部分字段加密并不適合多字段的加密存儲,容易導(dǎo)致加密數(shù)據(jù)太過分散,影響性能。所以推薦對整個(gè)數(shù)據(jù)庫加密。
2.KeyChain存儲敏感數(shù)據(jù)
KeyChain是iOS系統(tǒng)級的存儲方式,安全性無需質(zhì)疑,且刪除應(yīng)用或者升級系統(tǒng)依然可以保留里面的信息。
3、源碼安全
使用越獄設(shè)備可以很輕易地把應(yīng)用砸殼,從而把源碼dump下來,即使是沒有太多經(jīng)驗(yàn)的開發(fā)者也可以得到應(yīng)用的類信息,包括函數(shù)名等。使用IDA等反編譯工具可以看到應(yīng)用的一些類名和方法名,進(jìn)而可以分析功能實(shí)現(xiàn)的邏輯。
1.字符串混淆
對應(yīng)用程序中使用到的字符串進(jìn)行加密,保證源碼被逆向后也能保護(hù)明文字符串。
2.類名、方法名混淆
市面上很多iOS應(yīng)用都沒有混淆類名方法名,以致于很容易使用class-dump下來,從而進(jìn)行hook操作,一步一步實(shí)現(xiàn)iOS微信自動搶紅包(非越獄)是很有趣的一個(gè)應(yīng)用。這個(gè)庫可以混淆OC的類名、協(xié)議、屬性還有方法名。
3.程序結(jié)構(gòu)混淆加密
對應(yīng)用程序邏輯結(jié)構(gòu)進(jìn)行打亂混排,保證源碼可讀性降到最低。可參考這個(gè)庫
4.反調(diào)試、反注入等一些主動保護(hù)策略
加入第三方安全性SDK
二、對數(shù)據(jù)庫加密的研究
1、主流數(shù)據(jù)庫加密方式
The SQLite Encryption Extension(收費(fèi))
SQLiteEncrypt(收費(fèi))
SQLiteCrypt(收費(fèi))
SQLCipher(開源免費(fèi))
只有SQLCipher是免費(fèi)的,所以本文主要對SQLCipher進(jìn)行研究。
2、引入SQLCipher第三方庫
1.手動引入
請參考官方教程。
2.使用CocoaPods
pod 'FMDB/SQLCipher', '2.5'
3、SQLCipher的可行性研究
1.新建加密數(shù)據(jù)庫
若沒有舊數(shù)據(jù)的情況下使用很簡單,只需要在FMDatabase里面的-open和-openWithFlags:方法里面添加[self setKey:kDatabaseEncryptKey];即可。
如下圖所示

但是在團(tuán)隊(duì)協(xié)作中,如果直接修改pod倉庫里面的文件,可能不好同步,下面有一個(gè)技巧,就是繼承FMDatabase和FMDatabaseQueue并重載其中的方法。
繼承FMDatabase的子類需要重載以下方法
- (BOOL)open {
if (_db) {
return YES;
}
int err = sqlite3_open([self sqlitePath], &_db );
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
} else {
// 設(shè)置密鑰
[self setKey:kDatabaseEncryptKey];
}
if (_maxBusyRetryTimeInterval > 0.0) {
// set the handler
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}
return YES;
}
- (BOOL)openWithFlags:(int)flags {
if (_db) {
return YES;
}
int err = sqlite3_open_v2([self sqlitePath], &_db, flags, NULL /* Name of VFS module to use */);
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
} else {
// 設(shè)置密鑰
[self setKey:kDatabaseEncryptKey];
}
if (_maxBusyRetryTimeInterval > 0.0) {
// set the handler
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}
return YES;
}
- (const char*)sqlitePath {
if (!_databasePath) {
return ":memory:";
}
if ([_databasePath length] == 0) {
return ""; // this creates a temporary database (it's an sqlite thing).
}
return [_databasePath fileSystemRepresentation];
}
繼承FMDatabaseQueue的子類只需要重載一個(gè)方法
+ (Class)databaseClass {
return [FMEncryptDatabase class];
}
使用的時(shí)候只需要更改下面的語句
// 原:
self.normalQueue = [FMDatabaseQueue databaseQueueWithPath:self.normalDbPath];
// 改為:
self.encryptQueue = [FMEncryptDatabaseQueue databaseQueueWithPath:self.encryptDbPath];
2.加密已有數(shù)據(jù)庫
加密已有數(shù)據(jù)庫我已經(jīng)封裝成下面的方法:
/**
加密數(shù)據(jù)庫(保留原有數(shù)據(jù)庫)
*/
+ (BOOL)encryptDatabase:(NSString *)origPath toPath:(NSString *)toPath {
sqlite3 *db;
if (sqlite3_open([origPath UTF8String], &db) == SQLITE_OK) {
char *err = NULL;
sqlite3_exec(db, [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS encrypted KEY '%@';", toPath, kDatabaseEncryptKey] UTF8String], NULL, NULL, &err);
sqlite3_exec(db, "SELECT sqlcipher_export('encrypted');", NULL, NULL, &err);
sqlite3_exec(db, "DETACH DATABASE encrypted;", NULL, NULL, &err);
sqlite3_close(db);
return err ? NO : YES;
} else {
sqlite3_close(db);
NSLog(@"Open db failed:%s", sqlite3_errmsg(db));
return NO;
}
}
這里有兩點(diǎn)需要注意的地方:
1、這里必須使用全路徑
2、加密后的queue必須同步更換為FMEncryptDatabaseQueue,不然無法讀寫數(shù)據(jù)
3.解密已加密數(shù)據(jù)庫
解密已加密的數(shù)據(jù)庫已封裝為以下方法:
/**
解密數(shù)據(jù)庫(保留原有數(shù)據(jù)庫)
*/
+ (BOOL)unencryptDatabase:(NSString *)origPath toPath:(NSString *)toPath {
sqlite3 *db;
if (sqlite3_open([origPath UTF8String], &db) == SQLITE_OK) {
char *err = NULL;
sqlite3_exec(db, [[NSString stringWithFormat:@"PRAGMA key = '%@';", kDatabaseEncryptKey] UTF8String], NULL, NULL, &err);
sqlite3_exec(db, [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS plaintext KEY '';", toPath] UTF8String], NULL, NULL, NULL);
sqlite3_exec(db, "SELECT sqlcipher_export('plaintext');", NULL, NULL, &err);
sqlite3_exec(db, "DETACH DATABASE plaintext;", NULL, NULL, &err);
sqlite3_close(db);
return err ? NO : YES;
} else {
sqlite3_close(db);
NSLog(@"Open db failed:%s", sqlite3_errmsg(db));
return NO;
}
}
這里也有兩點(diǎn)需要注意的地方:
1、這里必須使用全路徑
2、解密后的queue必須同步更換為FMDatabaseQueue,不然無法讀寫數(shù)據(jù)
4、SQLCipher性能分析
本次性能分析主要從兩個(gè)維度下手:
1.對比無加密數(shù)據(jù)庫和加密數(shù)據(jù)庫的插入數(shù)據(jù)速度;
2.加密已有數(shù)據(jù)的數(shù)據(jù)庫耗時(shí)。
本次測試都基于iOS10.3的iPad mini2下進(jìn)行。
1.對比插入數(shù)據(jù)速度
規(guī)則:對比無加密數(shù)據(jù)庫和加密數(shù)據(jù)庫使用事務(wù)時(shí),在插入1w條、5w條和10w條8個(gè)字段的數(shù)據(jù)的耗時(shí),每組數(shù)據(jù)測試5次,最后取平均值。最后的測試結(jié)果如下圖所示:

結(jié)論:兩者相差在4%左右,在移動設(shè)備上存儲大數(shù)據(jù)的情況很少,所以我認(rèn)為是完全可以接受的。
2.測試加密數(shù)據(jù)庫耗時(shí)
規(guī)則:對比存儲1w條、5w條和10w條數(shù)據(jù)的數(shù)據(jù)庫加密的耗時(shí),每組數(shù)據(jù)測試5次,最后取平均值。最后的測試結(jié)果如下圖所示:

結(jié)論:加密過程需要一定的時(shí)間,且跟數(shù)據(jù)庫所存儲的數(shù)據(jù)量有關(guān),數(shù)據(jù)量越大耗時(shí)越長,從本次的測試結(jié)果來看,加密存儲10w條數(shù)據(jù)的數(shù)據(jù)庫耗時(shí)1.38s是可以接受的。
5、采用SQLCipher存在的風(fēng)險(xiǎn)
1、SQLCipher依賴本地存儲的字符串進(jìn)行數(shù)據(jù)庫的加密和解密,如果這個(gè)字符串被泄漏出去,本地的數(shù)據(jù)庫依然容易被解密,進(jìn)而被讀取里面的信息??刹捎肒eyChain保存?
2、加密已存儲大數(shù)據(jù)的數(shù)據(jù)庫會消耗一定的時(shí)間,這段時(shí)間怎么處理才能讓用戶無感知?加密失敗了怎么處理?
3、歡迎大家補(bǔ)充。。。