ios FMDB + SQLCipher

377 阅读3分钟

手动集成SQLCipher代码

从GitHub下载 SQLCipher到本地: github.com/sqlcipher/s…
把sqlite3.c和sqlite3.h加入到工程目录,然后在project的build setting里改动两个配置:

通过查询资料SQLite是否开启加密模块是通过宏(SQLITE_HAS_CODEC)来配置的。那么就需要在Xcode中配置开启SQLite加密组件的宏(如使用CocoaPods方式则不需要配置)。

(1)target –> Build Setting –> Other C Flags添加-DSQLITE_HAS_CODEC、-DSQLITE_TEMP_STORE=2、-DSQLITE_THREADSAFE、- DSQLCIPHER_CRYPTO_CC几项配置

(2)target –> Build Setting –> Other Linker Flags添加-framework Security配置

1)、other c flags下添加如下设置:

image.png 2)、 other link flags下添加如下设置:

image.png

报错问题:

如果出现sqlite3_key方法报错,提示如下:Implicit declaration of function 'sqlite3_key' is invalid in C99

则可能是因为搜索不到sqlite3.h,需要把sqlite3.c和sqlite3.h所在目录添加到header search path下面,如:

$(SRCROOT)/myproject/SQLCipher

1> 选择你的项目,右键,选择 "Add Files to [你的工程]";在弹出的选择窗口找到你刚才从git下载的sqlcipher路径,打开sqlcipher文件夹,选择sqlcipher.xcodeproj

image.png

image.png

2> 点击工程,选择TARGETS中你的工程,点击 Build Phases Tab栏,展开 Target Dependencies 点击 +

image.png

3> 添加 sqlcipher 静态库

image.png

4> 展开Link Binary With Libraries

image.png

5> 添加+libsqlcipher.a库

image.png

注:如果你的工程库中已经添加了 libsqlite3.dylib 或者其他的SQLite库,请Remove掉,否则可能会提示出现重复sqlite库

6> 回到你的工程编辑面板,选择工程,TARGETS 你的工程Target, Build Settings Tab栏,选择 Header Search Paths 项,双击键入新值: ./sqlcipher/src

image.png

7> 选择 Other C Flags ,双击 添加 -DSQLITE_HAS_CODEC

image.png

注:Release 和 Debug的配置值一样

现在就可以直接使用 sqlite3 加密数据库

#import "AppDelegate.h"
#import <sqlite3.h>

@interface AppDelegate ()

@property (nonatomic) BOOL isLoginViewControllerDisplayed;
@property (readonly) NSURL *databaseURL;
@property (readonly) BOOL databaseExists;

@end

@implementation AppDelegate

@dynamic databaseURL;
@dynamic databaseExists;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.


    // Set up a SQLCipher database connection:
    sqlite3 *db;
    if (sqlite3_open([[self.databaseURL path] UTF8String], &db) == SQLITE_OK)
    {
        const char* key = [@"StrongPassword" UTF8String];
        sqlite3_key(db, key, (int)strlen(key));
        if (sqlite3_exec(db, (const char*) "SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL) == SQLITE_OK)
        {
            NSLog(@"Password is correct, or a new database has been initialized");
        }
        else
        {
            NSLog(@"Incorrect password!");
        }
        sqlite3_close(db);
    }
    return YES;
}

- (NSURL *)databaseURL
{
    NSArray *URLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
    NSURL *directoryURL = [URLs firstObject];
    NSURL *databaseURL = [directoryURL URLByAppendingPathComponent:@"secure.db"];
    return  databaseURL;
}

- (BOOL)databaseExists
{
    BOOL exists = NO;
    NSError *error = nil;
    exists = [[self databaseURL] checkResourceIsReachableAndReturnError:&error];
    if (exists == NO && error != nil)
    {
        NSLog(@"Error checking availability of database file: %@", error);
    }
    return exists;
}
@end

使用cocopods集成SQLCipher

1、下载支持SQLCipher的FMDB分支

FMDB官方库地址:github.com/ccgus/fmdb

SQLCipher官网:www.zetetic.net/sqlcipher/

作者做了一个支持SQLCipher的仓库,为我们节约了大量编译和工程配置时间,感谢!

修改Podfile:

pod 'FMDB'

替换为

pod 'FMDB/SQLCipher'

更新一下Pod即可得到支持SQLCipher的FMDB。

2、加密方法的使用

- (BOOL)setKey:(NSString*)key;

方法很简单,关键是使用的位置:

2.1、直接使用FMDatabase

每次调用

[db open]

的时候调用一次

2.2、使用FMDatabaseQueue

调用

+ (instancetype)databaseQueueWithPath:(NSString*)aPath

的时候调用一次 ,例如:

self.dbQueue = [FMDatabaseQueue databaseQueueWithPath:dbPath]

[self.dbQueue inDatabase:^(FMDatabase*db) {

    [db setKey:kDBEncryptedKey]

}];

这里有个坑:不是每次调用 - (void)inDatabase:(void(^)(FMDatabase*db))block 的时候都要在block里调用 - (BOOL)setKey:(NSString*)key 只需要在初始化FMDatabaseQueue完的时候调用一次即可,而且要保证最先调用,不然任何数据库操作都是非法的!

3、非加密数据库<->加密数据库

如何将现有非加密数据库在保存数据的情况下转换为加密数据库?

又如何将加密数据库保留数据还原为非加密数据库?

这里需要用到 sqlcipher_export() 方法。

3.1、创建全新的加密数据库

如果待创建的数据库是全新的,按照上面说的方法直接调用

- (BOOL)setKey:(NSString*)key

即可得到加密数据库。

3.2、将现有非加密数据库转换为加密数据库

如果你的数据库开始是没有加密的,按照上面介绍的方法调用

- (BOOL)setKey:(NSString*)key


并不会将现有数据库转换为加密数据库,只会得到一大堆报错。

下面是一段从SQLCipher拷贝来的例子:

Example 1: Encrypt a Plaintext Database

$ ./sqlcipher plaintext.db

sqlite> ATTACH DATABASE 'encrypted.db' AS encrypted KEY 'testkey';

sqlite> SELECT sqlcipher_export('encrypted');

sqlite> DETACH DATABASE encrypted;

我们只需要执行三个SQL语句即可将非加密数据库拷贝到加密数据库,是不是很简单?但是这里又有个坑:

1、sqlite> ATTACH DATABASE 'encrypted.db' AS encrypted KEY 'testkey';

这句话中的'encrypted.db',代表待创建的加密数据库的全路径,并不只是数据库的名称;

2、sqlite> SELECT sqlcipher_export('encrypted');

这句话中的'encrypted',代表一个关键字!!!并不是什么待创建的数据库的全路径也不是什么数据库名称!或许真的是我太单纯了...

3、sqlite> DETACH DATABASE encrypted;

没错,这里的encrypted也是一个关键字

使用代码封装一下:

#define kDBEncryptedKey@"abcdefg"

NSString *encryptedDBPath = @"Your/DB/Path/encryptedDB.db"

NSString *plainttextDBPath = @"Your/DB/Path/plainttextDB.db"

const char *sql = [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS encrypted KEY '%@';", encryptedDBPath, kDBEncryptedKey] UTF8String];

const char *exportSql = [[NSString stringWithFormat:@"SELECT sqlcipher_export('encrypted');"] UTF8String];

const char *detachSql = [[NSString stringWithFormat:@"DETACH DATABASE encrypted;"] UTF8String];

sqlite3 *unencrypted_DB = NULL;

if(sqlite3_open([plainttextDBPath UTF8String], &unencrypted_DB) ==SQLITE_OK)

{

    int rc;

    char *errmsg = NULL;

    rc =sqlite3_exec(unencrypted_DB, sql,NULL,NULL, &errmsg);

    rc =sqlite3_exec(unencrypted_DB, exportSql,NULL,NULL, &errmsg);

    rc =sqlite3_exec(unencrypted_DB, detachSql,NULL,NULL, &errmsg);

    sqlite3_close(unencrypted_DB);

}else

{

    sqlite3_close(unencrypted_DB);

    NSAssert1(NO,@"Failed to open database with message '%s'.", sqlite3_errmsg(unencrypted_DB));

}

通过上面的代码可以从非加密数据库拷贝一份保留数据的加密数据库,加密数据库生成后记得删除旧数据库。

3.3、将加密数据库转换为非加密数据库

下面又是一段从SQLCipher拷贝来的例子:

Example 2: Decrypt a SQLCipher database to a Plaintext Database

$ ./sqlcipher encrypted.db

sqlite> PRAGMA key = 'testkey';

sqlite> ATTACH DATABASE 'plaintext.db' AS plaintext KEY '';  -- empty key will disable encryption

sqlite> SELECT sqlcipher_export('plaintext');

sqlite> DETACH DATABASE plaintext;

记得上面说的坑:

'plaintext.db'是待创建的未加密数据的全路径;

'plaintext'是关键字,不是路径也不是去掉后缀的数据库名称;

plaintext是关键字;


使用代码封装一下:

#define kDBEncryptedKey@"abcdefg"

NSString *encryptedDBPath = @"Your/DB/Path/encryptedDB.db"

NSString *plaintextDBPath = @"Your/DB/Path/plainttextDB.db"

const char *sql = [[NSStringstringWithFormat:@"PRAGMA key = '%@';", kDBEncryptedKey] UTF8String];

const char *attachSql = [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS plaintext KEY '';", plaintextDBPath] UTF8String];

const char *exportSql = [[NSStringstringWithFormat:@"SELECT sqlcipher_export('plaintext');"] UTF8String];

const char *detachSql = [[NSStringstringWithFormat:@"DETACH DATABASE plaintext;"] UTF8String];

sqlite3*encrypted_DB =NULL;

if(sqlite3_open([encryptedDBPathUTF8String], &encrypted_DB) ==SQLITE_OK)

{

    int rc;

    char *errmsg = NULL;

    rc = sqlite3_exec(encrypted_DB, sql,NULL,NULL, &errmsg);

    rc =sqlite3_exec(encrypted_DB, attachSql,NULL,NULL, &errmsg);

    rc =sqlite3_exec(encrypted_DB, exportSql,NULL,NULL, &errmsg);

    rc =sqlite3_exec(encrypted_DB, detachSql,NULL,NULL, &errmsg)

    sqlite3_close(encrypted_DB);

} else

{

    sqlite3_close(encrypted_DB);

    NSAssert1(NO,@"Failed to open database with message '%s'.", sqlite3_errmsg(encrypted_DB));

}

使用上面的代码就可以将加密后的数据库还原成未加密数据库,同样记得删除旧数据库。

坑:转换后的数据库除了保留了数据外,并没有保留以前的数据库附加信息,例如userVersion,记得将老数据库的userVersion赋给新数据库,不然数据库升级可能要有麻烦了!