2016-04-19更新:本文代碼可能有些問題,請移步 http://zhengbomo.github.io/2016-04-18/sqlcipher-start/ 查看
sqlite應用幾乎在所有的App都能看到,雖然我們的數據存儲在沙盒里面,一般情況下無法拿到,但是iOS管理軟件(如:iFunBox)可以讀取到應用程序沙盒里面的文件,為了提高數據的安全性,我們需要考慮對數據庫進行加密
數據庫加密一般有兩種方式
1、對所有數據進行加密
2、對數據庫文件加密
處於客戶端性能的考慮,通常我們對數據庫文件進行加密,在iOS上用的比較多的是 sqlcipher,由於原生提供的sqlite API是C語言實現的,通常我們會用一個在github上比較有名的一個工具庫FMDB,FMDB對原生的sqlite進行了封裝,提供了面向對象的方式對數據庫操作,同時FMDB 也提供了對 sqlcipher 的支持
下面基於 FMDB 和 sqlcipher 演示數據庫加解密
編譯sqlcipher需要做一些配置,具體配置詳情見:https://www.zetetic.net/sqlcipher/ios-tutorial/
我們通過 cocoapod 引用 FMDB 和sqlcipher 我們可以直接拿到編譯好的.a文件,直接用就可以
1、通過cocoapod 引用庫
pod 'FMDB/SQLCipher', '~> 2.5'
如果項目已經引用了FMDB,改為FMDB/SQLCipher 重新install一次即可,通過cocoapod添加的FMDB默認還是沒有加密的,要使用加密的功能,需要在數據庫open后調用setKey方法設置key,如下
- (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 { //數據庫open后設置加密key
[self setKey:encryptKey_]; } if (_maxBusyRetryTimeInterval > 0.0) { // set the handler
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; } return YES; }
關鍵代碼:[self setKey:encryptKey_];
2、添加數據庫加密操作類
上面代碼是FMDatabase中的,CocoaPod添加的庫不推薦修改,修改后不利於類庫的統一管理和更新
有些人則不用cocoapod引用FMDB,而是直接把FMDB的源文件拷貝到項目中,然后進行修改,我更傾向與保留cocoapod對FMDB的管理,通過新增類提供對數據庫加密的支持,這里新增兩個類:FMEncryptDatabase 和 FMEncryptDatabaseQueue
我們可以重用 FMDatabase 和 FMDatabaseQueue 的邏輯,所以我們可以繼承自他們,同時我再FMEncryptDatabase 中提供兩個數據庫遷移的方法,可以把未加密的數據庫轉換為加密的數據庫,也可以反向轉換
由於secretKey一般只需要一份,所以這里使用一個靜態變量實現,如果需要修改,可以在AppDelegate的 application:didFinishLaunchingWithOptions: 方法進行設置

#import "FMDatabase.h" @interface FMEncryptDatabase : FMDatabase /** 如果需要自定義encryptkey,可以調用這個方法修改(在使用之前)*/ + (void)setEncryptKey:(NSString *)encryptKey; @end @implementation FMEncryptDatabase static NSString *encryptKey_; + (void)initialize { [super initialize]; //初始化數據庫加密key,在使用之前可以通過 setEncryptKey 修改 encryptKey_ = @"FDLSAFJEIOQJR34JRI4JIGR93209T489FR"; } #pragma mark - 重載原來方法 - (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 { //數據庫open后設置加密key [self setKey:encryptKey_]; } if (_maxBusyRetryTimeInterval > 0.0) { // set the handler [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; } return YES; } #if SQLITE_VERSION_NUMBER >= 3005000 - (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 { //數據庫open后設置加密key [self setKey:encryptKey_]; } if (_maxBusyRetryTimeInterval > 0.0) { // set the handler [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; } return YES; } #endif - (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]; } #pragma mark - 配置方法 + (void)setEncryptKey:(NSString *)encryptKey { encryptKey_ = encryptKey; } @end

#import "FMDatabaseQueue.h" #import "FMEncryptDatabase.h" @interface FMEncryptDatabaseQueue : FMDatabaseQueue @end @implementation FMEncryptDatabaseQueue + (Class)databaseClass { return [FMEncryptDatabase class]; } @end

#import <Foundation/Foundation.h> #import "sqlite3.h" @interface FMEncryptHelper : NSObject /** 對數據庫加密 */ + (BOOL)encryptDatabase:(NSString *)path; /** 對數據庫解密 */ + (BOOL)unEncryptDatabase:(NSString *)path; /** 對數據庫加密 */ + (BOOL)encryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath; /** 對數據庫解密 */ + (BOOL)unEncryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath; /** 修改數據庫秘鑰 */ + (BOOL)changeKey:(NSString *)dbPath originKey:(NSString *)originKey newKey:(NSString *)newKey; @end @implementation FMEncryptHelper static NSString *encryptKey_; + (void)initialize { encryptKey_ = @"FDLSAFJEIOQJR34JRI4JIGR93209T489FR"; } //對數據庫加密(文件不變) + (BOOL)encryptDatabase:(NSString *)path { NSString *sourcePath = path; NSString *targetPath = [NSString stringWithFormat:@"%@.tmp.db", path]; if([self encryptDatabase:sourcePath targetPath:targetPath]) { NSFileManager *fm = [[NSFileManager alloc] init]; [fm removeItemAtPath:sourcePath error:nil]; [fm moveItemAtPath:targetPath toPath:sourcePath error:nil]; return YES; } else { return NO; } } //對數據庫解密(文件不變) + (BOOL)unEncryptDatabase:(NSString *)path { NSString *sourcePath = path; NSString *targetPath = [NSString stringWithFormat:@"%@.tmp.db", path]; if([self unEncryptDatabase:sourcePath targetPath:targetPath]) { NSFileManager *fm = [[NSFileManager alloc] init]; [fm removeItemAtPath:sourcePath error:nil]; [fm moveItemAtPath:targetPath toPath:sourcePath error:nil]; return YES; } else { return NO; } } /** 對數據庫加密 */ + (BOOL)encryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath { const char* sqlQ = [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS encrypted KEY '%@';", targetPath, encryptKey_] UTF8String]; sqlite3 *unencrypted_DB; if (sqlite3_open([sourcePath UTF8String], &unencrypted_DB) == SQLITE_OK) { // Attach empty encrypted database to unencrypted database sqlite3_exec(unencrypted_DB, sqlQ, NULL, NULL, NULL); // export database sqlite3_exec(unencrypted_DB, "SELECT sqlcipher_export('encrypted');", NULL, NULL, NULL); // Detach encrypted database sqlite3_exec(unencrypted_DB, "DETACH DATABASE encrypted;", NULL, NULL, NULL); sqlite3_close(unencrypted_DB); return YES; } else { sqlite3_close(unencrypted_DB); NSAssert1(NO, @"Failed to open database with message '%s'.", sqlite3_errmsg(unencrypted_DB)); return NO; } } /** 對數據庫解密 */ + (BOOL)unEncryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath { const char* sqlQ = [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS plaintext KEY '';", targetPath] UTF8String]; sqlite3 *encrypted_DB; if (sqlite3_open([sourcePath UTF8String], &encrypted_DB) == SQLITE_OK) { sqlite3_exec(encrypted_DB, [[NSString stringWithFormat:@"PRAGMA key = '%@';", encryptKey_] UTF8String], NULL, NULL, NULL); // Attach empty unencrypted database to encrypted database sqlite3_exec(encrypted_DB, sqlQ, NULL, NULL, NULL); // export database sqlite3_exec(encrypted_DB, "SELECT sqlcipher_export('plaintext');", NULL, NULL, NULL); // Detach unencrypted database sqlite3_exec(encrypted_DB, "DETACH DATABASE plaintext;", NULL, NULL, NULL); sqlite3_close(encrypted_DB); return YES; } else { sqlite3_close(encrypted_DB); NSAssert1(NO, @"Failed to open database with message '%s'.", sqlite3_errmsg(encrypted_DB)); return NO; } } /** 修改數據庫秘鑰 */ + (BOOL)changeKey:(NSString *)dbPath originKey:(NSString *)originKey newKey:(NSString *)newKey { sqlite3 *encrypted_DB; if (sqlite3_open([dbPath UTF8String], &encrypted_DB) == SQLITE_OK) { sqlite3_exec(encrypted_DB, [[NSString stringWithFormat:@"PRAGMA key = '%@';", originKey] UTF8String], NULL, NULL, NULL); sqlite3_exec(encrypted_DB, [[NSString stringWithFormat:@"PRAGMA rekey = '%@';", newKey] UTF8String], NULL, NULL, NULL); sqlite3_close(encrypted_DB); return YES; } else { sqlite3_close(encrypted_DB); NSAssert1(NO, @"Failed to open database with message '%s'.", sqlite3_errmsg(encrypted_DB)); return NO; } } @end
3、測試
好了,通過上面兩個類創建的數據庫都是加密過的,下面做一些測試,具體代碼見后面的demo
加密后的數據庫暫時沒有找到可以打開的GUI工具查看(MesaSQLite),即使輸入secretKey也無法查看,不知道為何
4、常見問題問題
如果你不是通過Cocoapod的方式引用的Fmdb和Sqlcipher,可以直接在https://github.com/sqlcipher/sqlcipher 下載到sqlcipher的工程文件,然后應用到項目中,並且需要在你的項目中添加-DSQLITE_HAS_CODEC 宏定義,否則使用Fmdb的時候將不會加密
5、Demo
http://files.cnblogs.com/files/bomo/FmdbEncryptDemo.zip
個人水平有限,如果你有更好的建議和實現方式,歡迎留言探討