天天看點

iOS 資料庫加密和資料遷移

一、 FMDB/SQLCipher資料庫加解密,遷移

介紹

使用SQLite資料庫的時候,有時候對于資料庫要求比較高,特别是在iOS8.3之前,未越獄的系統也可以通過工具拿到應用程式沙盒裡面的檔案,這個時候我們就可以考慮對SQLite資料庫進行加密,這樣就不用擔心sqlite檔案洩露了

通常資料庫加密一般有兩種方式

  1. 對所有資料進行加密
  2. 對資料庫檔案加密
第一種方式雖然加密了資料,但是并不完全,還是可以通過資料庫檢視到表結構等資訊,并且對于資料庫的資料,資料都是分散的,要對所有資料都進行加解密操作會嚴重影響性能,通常的做法是采取對檔案加密的方式

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方法設定秘鑰

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

為了不修改FMDB的源代碼,我們可以繼承自FMDatabase類重寫需要setKey的幾個方法,這裡我繼承FMDatabase定義了一個 

FMEncryptDatabase

 類,提供打開加密檔案的功能(具體定義見 Demo )

@interface FMEncryptDatabase : FMDatabase

+ (instancetype)databaseWithPath:(NSString*)aPath encryptKey:(NSString *)encryptKey;
- (instancetype)initWithPath:(NSString*)aPath encryptKey:(NSString *)encryptKey;

@end
      

用法與FMDatabase一樣,隻是需要傳入secretKey

SQLite資料庫加解密

SQLCipher提供了幾個指令用于加解密操作

加密

$ ./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. 解除安裝新資料庫

解密

$ ./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. 解除安裝新資料庫

代碼操作

/** 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

繼續閱讀