SQLCipher对基于FMDB的sqlite数据库加密

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

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

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

  • 对所有数据进行加密
  • 对数据库文件加密

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

集成

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

1
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
    - (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
    }

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

  • 通过客户端验证是否可以读取数据库信息
  1. 取到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
5
$ ./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;
  • 打开加密数据库
  • 创建一个新的不加密的数据库附加到原数据库上
  • 导出数据到新数据库上
  • 卸载新数据库

代码操作

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/** 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上下载咯

参考:

————————-更新————————

查看加密后的数据库,可以使用DB Browser for SQLite

  • 打开客户端->打开数据库->输入密码

  • 查看数据库中的表结构