• 【iOS】FMDB/SQLCipher数据库加解密,迁移


    2016-04-19更新:本文代码可能有些问题,请移步 http://zhengbomo.github.io/2016-04-18/sqlcipher-start/ 查看

      sqlite应用几乎在所有的App都能看到,虽然我们的数据存储在沙盒里面,一般情况下无法拿到,但是iOS管理软件(如:iFunBox)可以读取到应用程序沙盒里面的文件,为了提高数据的安全性,我们需要考虑对数据库进行加密

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

        1、对所有数据进行加密

        2、对数据库文件加密

      处于客户端性能的考虑,通常我们对数据库文件进行加密,在iOS上用的比较多的是 sqlcipher,由于原生提供的sqlite API是C语言实现的,通常我们会用一个在github上比较有名的一个工具库FMDB,FMDB对原生的sqlite进行了封装,提供了面向对象的方式对数据库操作,同时FMDB 也提供了对 sqlcipher 的支持

      下面基于 FMDB 和 sqlcipher 演示数据库加解密

      编译sqlcipher需要做一些配置,具体配置详情见:https://www.zetetic.net/sqlcipher/ios-tutorial/

      我们通过 cocoapod 引用 FMDB 和sqlcipher 我们可以直接拿到编译好的.a文件,直接用就可以

      

    1、通过cocoapod 引用库

    pod 'FMDB/SQLCipher', '~> 2.5'

      如果项目已经引用了FMDB,改为FMDB/SQLCipher 重新install一次即可,通过cocoapod添加的FMDB默认还是没有加密的,要使用加密的功能,需要在数据库open后调用setKey方法设置key,如下

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

      关键代码:[self setKey:encryptKey_];

    2、添加数据库加密操作类

      上面代码是FMDatabase中的,CocoaPod添加的库不推荐修改,修改后不利于类库的统一管理和更新

      有些人则不用cocoapod引用FMDB,而是直接把FMDB的源文件拷贝到项目中,然后进行修改,我更倾向与保留cocoapod对FMDB的管理,通过新增类提供对数据库加密的支持,这里新增两个类:FMEncryptDatabase 和 FMEncryptDatabaseQueue

      我们可以重用 FMDatabase 和 FMDatabaseQueue 的逻辑,所以我们可以继承自他们,同时我再FMEncryptDatabase 中提供两个数据库迁移的方法,可以把未加密的数据库转换为加密的数据库,也可以反向转换

      由于secretKey一般只需要一份,所以这里使用一个静态变量实现,如果需要修改,可以在AppDelegate的 application:didFinishLaunchingWithOptions: 方法进行设置

    #import "FMDatabase.h"
    
    @interface FMEncryptDatabase : FMDatabase
    
    /** 如果需要自定义encryptkey,可以调用这个方法修改(在使用之前)*/
    + (void)setEncryptKey:(NSString *)encryptKey;
    
    @end
    
    
    @implementation FMEncryptDatabase
    
    static NSString *encryptKey_;
    
    + (void)initialize
    {
        [super initialize];
        //初始化数据库加密key,在使用之前可以通过 setEncryptKey 修改
        encryptKey_ = @"FDLSAFJEIOQJR34JRI4JIGR93209T489FR";
    }
    
    #pragma mark - 重载原来方法
    - (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;
    }
    
    #if SQLITE_VERSION_NUMBER >= 3005000
    - (BOOL)openWithFlags:(int)flags {
        if (_db) {
            return YES;
        }
        
        int err = sqlite3_open_v2([self sqlitePath], &_db, flags, NULL /* Name of VFS module to use */);
        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;
    }
    
    #endif
    
    - (const char*)sqlitePath {
        
        if (!_databasePath) {
            return ":memory:";
        }
        
        if ([_databasePath length] == 0) {
            return ""; // this creates a temporary database (it's an sqlite thing).
        }
        
        return [_databasePath fileSystemRepresentation];
        
    }
    
    #pragma mark - 配置方法
    + (void)setEncryptKey:(NSString *)encryptKey
    {
        encryptKey_ = encryptKey;
    }
    
    @end
    FMEncryptDatabase
    #import "FMDatabaseQueue.h"
    #import "FMEncryptDatabase.h"
    
    @interface FMEncryptDatabaseQueue : FMDatabaseQueue
    
    @end
    
    
    @implementation FMEncryptDatabaseQueue
    
    + (Class)databaseClass
    {
        return [FMEncryptDatabase class];
    }
    
    @end
    FMEncryptDatabaseQueue
    #import <Foundation/Foundation.h>
    #import "sqlite3.h"
    
    @interface FMEncryptHelper : NSObject
    
    /** 对数据库加密 */
    + (BOOL)encryptDatabase:(NSString *)path;
    
    /** 对数据库解密 */
    + (BOOL)unEncryptDatabase:(NSString *)path;
    
    /** 对数据库加密 */
    + (BOOL)encryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath;
    
    /** 对数据库解密 */
    + (BOOL)unEncryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath;
    
    /** 修改数据库秘钥 */
    + (BOOL)changeKey:(NSString *)dbPath originKey:(NSString *)originKey newKey:(NSString *)newKey;
    
    @end
    
    
    @implementation FMEncryptHelper
    
    static NSString *encryptKey_;
    
    + (void)initialize
    {
        encryptKey_ = @"FDLSAFJEIOQJR34JRI4JIGR93209T489FR";
    }
    
    //对数据库加密(文件不变)
    + (BOOL)encryptDatabase:(NSString *)path
    {
        NSString *sourcePath = path;
        NSString *targetPath = [NSString stringWithFormat:@"%@.tmp.db", path];
        
        if([self encryptDatabase:sourcePath targetPath:targetPath]) {
            NSFileManager *fm = [[NSFileManager alloc] init];
            [fm removeItemAtPath:sourcePath error:nil];
            [fm moveItemAtPath:targetPath toPath:sourcePath error:nil];
            return YES;
        } else {
            return NO;
        }
    }
    
    //对数据库解密(文件不变)
    + (BOOL)unEncryptDatabase:(NSString *)path
    {
        NSString *sourcePath = path;
        NSString *targetPath = [NSString stringWithFormat:@"%@.tmp.db", path];
        
        if([self unEncryptDatabase:sourcePath targetPath:targetPath]) {
            NSFileManager *fm = [[NSFileManager alloc] init];
            [fm removeItemAtPath:sourcePath error:nil];
            [fm moveItemAtPath:targetPath toPath:sourcePath error:nil];
            return YES;
        } else {
            return NO;
        }
    }
    
    /** 对数据库加密 */
    + (BOOL)encryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath
    {
        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) {
            
            // Attach empty encrypted database to unencrypted database
            sqlite3_exec(unencrypted_DB, sqlQ, NULL, NULL, NULL);
            
            // export database
            sqlite3_exec(unencrypted_DB, "SELECT sqlcipher_export('encrypted');", NULL, NULL, NULL);
            
            // Detach encrypted database
            sqlite3_exec(unencrypted_DB, "DETACH DATABASE encrypted;", NULL, NULL, NULL);
            
            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;
        }
    }
    
    /** 对数据库解密 */
    + (BOOL)unEncryptDatabase:(NSString *)sourcePath targetPath:(NSString *)targetPath
    {
        const char* sqlQ = [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS plaintext KEY '';", targetPath] UTF8String];
        
        sqlite3 *encrypted_DB;
        if (sqlite3_open([sourcePath UTF8String], &encrypted_DB) == SQLITE_OK) {
            
            
            sqlite3_exec(encrypted_DB, [[NSString stringWithFormat:@"PRAGMA key = '%@';", encryptKey_] UTF8String], NULL, NULL, NULL);
            
            // Attach empty unencrypted database to encrypted database
            sqlite3_exec(encrypted_DB, sqlQ, NULL, NULL, NULL);
            
            // export database
            sqlite3_exec(encrypted_DB, "SELECT sqlcipher_export('plaintext');", NULL, NULL, NULL);
            
            // Detach unencrypted database
            sqlite3_exec(encrypted_DB, "DETACH DATABASE plaintext;", 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;
        }
    }
    
    /** 修改数据库秘钥 */
    + (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;
        }
    }
    
    @end
    FMEncryptHelper

    3、测试

      好了,通过上面两个类创建的数据库都是加密过的,下面做一些测试,具体代码见后面的demo

       加密后的数据库暂时没有找到可以打开的GUI工具查看(MesaSQLite),即使输入secretKey也无法查看,不知道为何

    4、常见问题问题

      如果你不是通过Cocoapod的方式引用的Fmdb和Sqlcipher,可以直接在https://github.com/sqlcipher/sqlcipher 下载到sqlcipher的工程文件,然后应用到项目中,并且需要在你的项目中添加-DSQLITE_HAS_CODEC 宏定义,否则使用Fmdb的时候将不会加密

    5、Demo

      http://files.cnblogs.com/files/bomo/FmdbEncryptDemo.zip

      个人水平有限,如果你有更好的建议和实现方式,欢迎留言探讨

  • 相关阅读:
    水晶报表 注册码
    黑马孕育期盘口的技术辨识(转贴)
    如何判断庄家出货(转贴)
    解决方案:用户 'sa' 登录失败。原因: 未与信任 SQL Server 连接相关联。
    水晶报表学习资料
    (网上收集)asp.net页面打印问题?
    arcgis地理配准第二种方法:利用已知控制点 (Spatial Adjustment和Georeferencing的区别)
    Vue父子组件之间通信
    Vue怎么引用组件和使用组件?
    ESLint:error 'reject' is defined but never used nounusedvars
  • 原文地址:https://www.cnblogs.com/bomo/p/4693076.html
Copyright © 2020-2023  润新知