OCSQLCipher

背景

数据安全越来越成为生活中非常重要的问题,用户隐私泄露的问题时有发生,如何保护用户数据的安全,越来越成为一个负责公司有限考虑的问题。本篇文章不涉及接口安全、传输安全等方面,只是对App使用的sqlite数据库安全做分析。

介绍

使用SQLite数据库的时候,有时候对于数据库要求比较高,特别是在iOS8.3之前,未越狱的系统也可以通过工具拿到应用程序沙盒里面的文件,这个时候我们就可以考虑对SQLite数据库进行加密,这样就不用担心sqlite文件泄露了

通常数据库加密一般有两种方式

对所有数据进行加密
对数据库文件加密
第一种方式虽然加密了数据,但是并不完全,还是可以通过数据库查看到表结构等信息,并且对于数据库的数据,数据都是分散的,要对所有数据都进行加解密操作会严重影响性能,通常的做法是采取对文件加密的方式

集成

LZ接手的项目原来使用cocoapod导入过FMDB,因为项目本身SVN管理的问题,原本不打算使用cocoapod导入SQLCipher,但是尝试过网上的解决方案但是没有加密成功,最后还是使用cocoapod导入的。

pod ‘FMDB/SQLCipher’, ‘~> 2.6.2’

确保自己的cocoapod版本号可以导入该库,LZ的cocoapod库原本是0.0.39版本的,老是报错,最后只得升级,如果原来的版本低于1.0.0,要注意修改podfile文件的格式

导入成功后,打开FMDatabase.m文件,找到下面的一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- (BOOL)open {
if (_db) {
return YES;
}

int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db );
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
}

if (_maxBusyRetryTimeInterval > 0.0) {
// set the handler
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}else{
//数据库open后设置加密key
[self setKey:encryptKey_];
}
return YES;
}

- (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName {

# if SQLITE_VERSION_NUMBER >= 3005000

if (_db) {
return YES;
}

int err = sqlite3_open_v2([self sqlitePath], (sqlite3**)&_db, flags, [vfsName UTF8String]);
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;

# else

NSLog(@"openWithFlags requires SQLite 3.5");
return NO;

# endif

}

到这一步就可以在初始化数据库时对数据库进行加密

通过客户端验证是否可以读取数据库信息
取到app中的数据库
真机连接xcode->window->Devices

将会得到下面的文件,然后右键显示包含内容,找到自己创建的数据库

打开数据库的工具,笔者使用的是Navicat Premium

打开Navicat Premium,链接数据库,选择sqlite

选中本地保存的数据库文件,点击ok打开

将会提示

提醒

注意:笔者这里修改的是源码,是因为接手的工程FMDB又被封装,更改起来比较困难,为了不修改FMDB的源代码,可以继承自FMDatabase类重写需要setKey的几个方法,具体写法可见demo,到这一步就可以在初始化数据库时对数据库进行加密,不过很多情况下,我们可能会遇到对已经存在的数据库进行加密

SQLite数据库加解密

SQLCipher提供了几个命令用于加解密操作

加密

1
2
3
4
$ ./sqlcipher plaintext.db  
sqlite> ATTACH DATABASE 'encrypted.db' AS encrypted KEY 'testkey';
sqlite> SELECT sqlcipher_export('encrypted');
sqlite> DETACH DATABASE encrypted;
  • 打开非加密数据库
  • 创建一个新的加密的数据库附加到原数据库上
  • 导出数据到新数据库上
  • 卸载新数据库

解密

1
2
3
4
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;
  • 打开加密数据库
  • 创建一个新的不加密的数据库附加到原数据库上
  • 导出数据到新数据库上
  • 卸载新数据库

代码操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/** encrypt sqlite database to new file */

+ (BOOL)encryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath encryptKey:(NSString *)encryptKey
{
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) {
char *errmsg;
// Attach empty encrypted database to unencrypted database
sqlite3_exec(unencrypted_DB, sqlQ, NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"%@", [NSString stringWithUTF8String:errmsg]);
sqlite3_close(unencrypted_DB);
return NO;
}
// export database
sqlite3_exec(unencrypted_DB, "SELECT sqlcipher_export('encrypted');", NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"%@", [NSString stringWithUTF8String:errmsg]);
sqlite3_close(unencrypted_DB);
return NO;
}
// Detach encrypted database
sqlite3_exec(unencrypted_DB, "DETACH DATABASE encrypted;", NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"%@", [NSString stringWithUTF8String:errmsg]);
sqlite3_close(unencrypted_DB);
return NO;
}
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;
}
}
/** decrypt sqlite database to new file */
+ (BOOL)unEncryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath encryptKey:(NSString *)encryptKey
{
const char* sqlQ = [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS plaintext KEY '';", targetPath] UTF8String];
sqlite3 *encrypted_DB;
if (sqlite3_open([sourcePath UTF8String], &encrypted_DB) == SQLITE_OK) {
char* errmsg;
sqlite3_exec(encrypted_DB, [[NSString stringWithFormat:@"PRAGMA key = '%@';", encryptKey] UTF8String], NULL, NULL, &errmsg);
// Attach empty unencrypted database to encrypted database
sqlite3_exec(encrypted_DB, sqlQ, NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"%@", [NSString stringWithUTF8String:errmsg]);
sqlite3_close(encrypted_DB);
return NO;
}
// export database
sqlite3_exec(encrypted_DB, "SELECT sqlcipher_export('plaintext');", NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"%@", [NSString stringWithUTF8String:errmsg]);
sqlite3_close(encrypted_DB);
return NO;
}
// Detach unencrypted database
sqlite3_exec(encrypted_DB, "DETACH DATABASE plaintext;", NULL, NULL, &errmsg);
if (errmsg) {
NSLog(@"%@", [NSString stringWithUTF8String:errmsg]);
sqlite3_close(encrypted_DB);
return NO;
}
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;
}
}
/** change secretKey for sqlite database */
+ (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;
}
}

结尾

SQLCipher使用起来还是很方便的,基本上不需要怎么配置,需要注意的是,尽量不要在操作过程中修改secretKey,否则,可能导致读不了数据,在使用第三方库的时候尽量不去修改源代码,可以通过扩展或继承的方式修改原来的行为,这样第三方库代码可以与官方保持一致,可以跟随官方版本升级,具体代码可以到github上下载