一、 FMDB/SQLCipher資料庫加解密,遷移
介紹
使用SQLite資料庫的時候,有時候對于資料庫要求比較高,特别是在iOS8.3之前,未越獄的系統也可以通過工具拿到應用程式沙盒裡面的檔案,這個時候我們就可以考慮對SQLite資料庫進行加密,這樣就不用擔心sqlite檔案洩露了
通常資料庫加密一般有兩種方式
- 對所有資料進行加密
- 對資料庫檔案加密
第一種方式雖然加密了資料,但是并不完全,還是可以通過資料庫檢視到表結構等資訊,并且對于資料庫的資料,資料都是分散的,要對所有資料都進行加解密操作會嚴重影響性能,通常的做法是采取對檔案加密的方式
iOS 免費版的sqlite庫并不提供了加密的功能,SQLite隻提供了加密的接口,但并沒有實作,iOS上支援的加密庫有下面幾種
- The SQLite Encryption Extension (SEE)
- 收費,有以下幾種加密方式
RC4
AES-128 in OFB mode
AES-128 in CCM mode
AES-256 in OFB mode
- 收費,有以下幾種加密方式
- SQLiteEncrypt
- 收費,使用AES加密
- SQLiteCrypt
- 收費,使用256-bit AES加密
- SQLCipher
- 開源,托管在github上,實作了SQLite官方的加密接口,也加了一些新的接口,詳情參見這裡
前三種都是收費的,SQLCipher是開源的,這裡我們使用SQLCipher
內建
如果你使用cocoapod的話就不需要自己配置了,為了友善,我們直接使用FMDB進行操作資料庫,FMDB也支援SQLCipher
pod ‘FMDB/SQLCipher’, ‘~> 2.6.2’
打開加密資料庫
使用方式與原來的方式一樣,隻需要資料庫open之後調用setKey設定一下秘鑰即可
下面摘了一段FMDatabase的open函數,在sqlite3_open成功後調用setKey方法設定秘鑰
|
為了不修改FMDB的源代碼,我們可以繼承自FMDatabase類重寫需要setKey的幾個方法,這裡我繼承FMDatabase定義了一個
FMEncryptDatabase
類,提供打開加密檔案的功能(具體定義見 Demo )
|
用法與FMDatabase一樣,隻是需要傳入secretKey
SQLite資料庫加解密
SQLCipher提供了幾個指令用于加解密操作
加密
|
- 打開非加密資料庫
- 建立一個新的加密的資料庫附加到原資料庫上
- 導出資料到新資料庫上
- 解除安裝新資料庫
解密
|
- 打開加密資料庫
- 建立一個新的不加密的資料庫附加到原資料庫上
- 導出資料到新資料庫上
- 解除安裝新資料庫
代碼操作
/** 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上下載下傳咯
參考
- http://www.cocoachina.com/industry/20140522/8517.html
- https://www.zetetic.net/sqlcipher/
原文位址:http://blog.bomobox.org/2016-04-18/sqlcipher-start/
二、 iOS SQLite 資料庫遷移
依據
sqlite有alter指令,可以增加字段。以下為代碼片段:
//對于老使用者,在資料庫表中增加字段
char *errMsg;
NSString *searchSql = [NSString stringWithFormat:@"select sql from sqlite_master where tbl_name='表名' and type='table'"];
const char *sql_Txt = [searchSql UTF8String];
sqlite3_prepare_v2(資料庫, sql_Txt, -1, &statement, NULL);
if(sqlite3_step(statement) == SQLITE_ROW){
char *sqlTxt= (char *)sqlite3_column_text(statement,0);
NSString *sqlString = [[NSString alloc] initWithUTF8String:sqlTxt];
// NSLog(@"%@", sqlString);
if ([sqlString rangeOfString: @"stockCode"].length <= 0 ) {
// NSLog(@"%@", @"沒有找到字段");
const char *sql_add = "ALTER TABLE 表名 ADD 字段名 字段類型";
if (sqlite3_exec(資料庫, sql_add, NULL, NULL, &errMsg)!=SQLITE_OK) {
// NSLog(@"%@", @"成功插入字段");
}
}
sqlite3_finalize(statement);
}
實作
最近不得不考慮關于資料庫遷移的問題,原先用了種很不好的處理方式(每次版本更新就删除本地資料庫,太傻),于是開始考慮下如何遷移資料庫。
項目使用的 FMDB ,除了使用 Core Data 外,這就是最好的了(最近好像又有了個 realm )。
在 FMDB 介紹頁面,發現了 FMDBMigrationManager ,大喜。
看了半天文檔,搗鼓了半天才弄出來,一步步整理下。
0.安裝 FMDBMigrationManager
Podfile 檔案:
platform :ios, "7.0"
pod 'FMDB'
pod 'FMDBMigrationManager'
使用
pod install
指令安裝
1.FMDBMigrationManager 建立資料庫
FMDBMigrationManager *manager = [FMDBMigrationManager managerWithDatabaseAtPath:[YMDatabaseHelper databasePath] migrationsBundle:[NSBundle mainBundle]];
其中
[YMDatabaseHelper databasePath]
是資料庫路徑
2.建立遷移表
BOOL resultState = [manager createMigrationsTable:&error];
建立的遷移表名稱為:
schema_migrations
3.建立 .sql 檔案
該檔案用來存儲每次更新使用的 SQL 語句。
FMDBMigrationManager
建議我們使用時間戳來作為版本号,使用下面的指令生成一個檔案:
touch "`ruby -e "puts Time.now.strftime('%Y%m%d%H%M%S%3N').to_i"`"_CreateMyAwesomeTable.sql
我生成的檔案名為:
20150420170044940_CreateMyAwesomeTable.sql
,其中
20150420170044940
為遷移的版本号辨別。
我們在
20150420170044940_CreateMyAwesomeTable.sql
檔案中建立一個使用者表,寫入:
CREATE TABLE User(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT
);
4.遷移函數
FMDBMigrationManager *manager = [FMDBMigrationManager managerWithDatabaseAtPath:[YMDatabaseHelper databasePath] migrationsBundle:[NSBundle mainBundle]];
BOOL resultState = NO;
NSError *error = nil;
if (!manager.hasMigrationsTable) {
resultState = [manager createMigrationsTable:&error];
}
resultState = [manager migrateDatabaseToVersion:UINT64_MAX progress:nil error:&error];//遷移函數
NSLog(@"Has `schema_migrations` table?: %@", manager.hasMigrationsTable ? @"YES" : @"NO");
NSLog(@"Origin Version: %llu", manager.originVersion);
NSLog(@"Current version: %llu", manager.currentVersion);
NSLog(@"All migrations: %@", manager.migrations);
NSLog(@"Applied versions: %@", manager.appliedVersions);
NSLog(@"Pending versions: %@", manager.pendingVersions);
UINT64_MAX
表示把資料庫遷移到最大的版本
運作項目,列印出如下内容:
-- :: YMFMDatabase[:] Has `schema_migrations` table?: YES
-- :: YMFMDatabase[:] Origin Version:
-- :: YMFMDatabase[:] Current version:
-- :: YMFMDatabase[:] All migrations: (
"<FMDBFileMigration: 0x17003b4c0>"
)
-- :: YMFMDatabase[:] Applied versions: (
)
-- :: YMFMDatabase[:] Pending versions: (
)
用 iFunBox 檢視下是不是建立了一個
User
表,裡面含有
id
、
name
字段。以及
FMDBMigrationManager
生成的
schema_migrations
表。
5.建立第二個 .sql 檔案
先用上方指令:
touch "`ruby -e "puts Time.now.strftime('%Y%m%d%H%M%S%3N').to_i"`"_CreateMyAwesomeTable.sql
生成,我生成的是:
20150420170557221_CreateMyAwesomeTable.sql
。
第二個 sql 檔案,裡面建立一個新表名字為
Grouping
,為原先的
User
表添加郵箱字段:
email
。.sql 檔案修改如下:
CREATE TABLE Grouping(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT
);
ALTER TABLE User ADD email TEXT;
6.建立第三個 .sql 檔案
生成同上,檔案内容是為
Grouping
表添加備注字段
remark
:
檔案内容:
OK,直接運作項目,看看是不是建立了
Grouping
表,裡面含有
id
,
name
,
remark
字段,以及
User
表裡面是不是添加了
email
字段。
7.遇到的問題
中間我自己做 Demo 時,試圖删除表中的某列,比如删除
User
表中的
name
列,但是不能成功,Google 了下發現答案。
SQLite supports a limited subset of ALTER TABLE. The ALTER TABLE command in SQLite allows the user to rename a table or to add a new column to an existing table. It is not possible to rename a column, remove a column, or add or remove constraints from a table.
解釋下:就是說
SQLite
對
ALERT TABLE
指令受限制,
SQLite
中的
ALERT TABLE
指令隻能允許使用者重命名表或者添加新列,不能重命名列或者删除列或者删除限制。
原文連結:http://www.jianshu.com/p/c19dd08697bd