【iOS】FMDB/SQLCipher數據庫加解密,遷移


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
FMEncryptDatabase
#import "FMDatabaseQueue.h"
#import "FMEncryptDatabase.h"

@interface FMEncryptDatabaseQueue : FMDatabaseQueue

@end


@implementation FMEncryptDatabaseQueue

+ (Class)databaseClass
{
    return [FMEncryptDatabase class];
}

@end
FMEncryptDatabaseQueue

 

#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
FMEncryptHelper

 

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

 

  個人水平有限,如果你有更好的建議和實現方式,歡迎留言探討


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM