对 SQL 数据库使用加密

Adobe AIR 1.5 和更高版本

所有 Adobe AIR 应用程序都共享同一个本地数据库引擎。因此,任何 AIR 应用程序都可以连接到、读取和写入未加密的数据库文件。从 Adobe AIR 1.5 起,AIR 中加入了创建和连接到加密数据库文件的功能。使用加密数据库时,应用程序必须提供正确的加密密钥才能连接到数据库。如果提供的加密密钥有误(或不提供密钥),则应用程序无法连接到数据库。因此,应用程序无法从数据库中读取数据,也无法写入数据库或更改数据库中的数据。

若要使用加密数据库,创建数据库时必须将其创建为加密数据库。有了加密数据库,即可打开到数据库的连接。还可以更改加密数据库的加密密钥。除了创建和连接到加密数据库之外,处理加密数据库的方法与处理未加密数据库的方法相同。尤其是无论数据库是否加密,执行 SQL 语句的方式都相同。

加密数据库的用途

希望限制对数据库中所存储信息的访问时,加密很有帮助。Adobe AIR 的数据库加密功能可以用于多种用途。下面是需要使用加密数据库的一些示例情况:

  • 从服务器下载的专用应用程序数据的只读缓存

  • 与服务器进行同步(向服务器发送数据以及从服务器加载数据)的专用数据的本地应用程序存储区

  • 用作由应用程序创建和编辑的文档的文件格式的加密文件。可以专用于一个用户、或可以在应用程序的所有用户中共享的文件。

  • 本地数据存储区的任何其他用途(如 本地 SQL 数据库的用途 中所述),在这些用途中不能向有权访问计算机或数据库文件的人员公开数据。

了解需要使用加密数据库的原因有助于您确定构建应用程序的方式。特别是,它可影响您的应用程序为数据库创建、获取及存储加密密钥的方式。有关这些注意事项的详细信息,请参阅 对数据库使用加密的注意事项

除了加密数据库, 加密本地存储区 是用于保存私有敏感数据的另一种机制。使用加密本地存储区,可使用字符串密钥存储单一 ByteArray 值。只有存储该值的 AIR 应用程序可访问它,而且只能在存储该值的计算机上进行访问。在采用加密本地存储区的情况下,不需要创建自己的加密密钥。 出于这些原因,加密本地存储区最适合于方便地存储易于在 ByteArray 中编码的一个值或一组值。加密数据库最适合于需要结构化数据存储和查询的大型数据集。有关使用加密本地存储的详细信息,请参阅 加密的本地存储区

创建加密数据库

若要使用加密数据库,则创建数据库文件时必须将其加密。 一旦在不加密的情况下创建数据库,以后就无法再对其进行加密。同样,以后也无法对加密数据库进行解密。如有必要,可以更改加密数据库的加密密钥。有关详细信息,请参阅 更改数据库的加密密钥 。如果现有的数据库未加密,而您又希望使用数据库加密,则可以新建一个加密数据库,然后将现有的表结构和数据复制到新数据库中。

创建加密数据库与创建未加密数据库几乎完全相同,如 创建数据库 中所述。首先创建一个表示数据库连接的 SQLConnection 实例。通过调用 SQLConnection 对象的 open() 方法或 openAsync() 方法创建数据库,并为数据库位置指定一个尚未存在的文件。创建加密数据库时的唯一区别在于要为 encryptionKey 参数( open() 方法的第五个参数和 openAsync() 方法的第六个参数)提供值。

有效的 encryptionKey 参数值为正好包含 16 个字节的 ByteArray 对象。

下列示例演示创建加密数据库。为简洁起见,在这些示例中,加密密钥在应用程序代码中采用硬编码形式。但是,由于此方法不安全,强烈建议不要使用此方法。

var conn:SQLConnection = new SQLConnection(); 
     
var encryptionKey:ByteArray = new ByteArray(); 
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure! 
     
// Create an encrypted database in asynchronous mode 
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey); 
     
// Create an encrypted database in synchronous mode 
conn.open(dbFile, SQLMode.CREATE, false, 1024, encryptionKey);

有关介绍生成加密密钥的建议方式的示例,请参阅 示例:生成和使用加密密钥

连接到加密数据库

与创建加密数据库类似的是,打开到加密数据库的连接所采用的步骤类似于连接到未加密数据库。该步骤在 连接到数据库 中有更详细的说明。使用 open() 方法 在同步执行模式下打开连接 ,或者使用 openAsync() 方法 在异步执行模式下打开连接 。唯一的区别在于,若要打开加密数据库,要为 encryptionKey 参数( open() 方法的第五个参数和 openAsync() 方法的第六个参数)指定正确的值。

如果所提供的加密密钥有误,则会出错。对于 open() 方法,将引发 SQLError 异常。对于 openAsync() 方法, SQLConnection 对象将调度 SQLErrorEvent ,其 error 属性包含 SQLError 对象。在任一情况下,由异常生成的 SQLError 对象的 errorID 属性值都为 3138。该错误 ID 对应于错误消息“所打开的文件不是数据库文件”。

以下示例介绍以异步执行模式打开加密数据库。为了简单起见,此示例中的加密密钥在应用程序代码中采用硬编码形式。但是,由于此方法不安全,强烈建议不要使用此方法。

import flash.data.SQLConnection; 
import flash.data.SQLMode; 
import flash.events.SQLErrorEvent; 
import flash.events.SQLEvent; 
import flash.filesystem.File; 
     
var conn:SQLConnection = new SQLConnection(); 
conn.addEventListener(SQLEvent.OPEN, openHandler); 
conn.addEventListener(SQLErrorEvent.ERROR, errorHandler); 
var dbFile:File = File.applicationStorageDirectory.resolvePath("DBSample.db"); 
     
var encryptionKey:ByteArray = new ByteArray(); 
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure! 
     
conn.openAsync(dbFile, SQLMode.UPDATE, null, false, 1024, encryptionKey); 
     
function openHandler(event:SQLEvent):void 
{ 
    trace("the database opened successfully"); 
} 
     
function errorHandler(event:SQLErrorEvent):void 
{ 
    if (event.error.errorID == 3138) 
    { 
        trace("Incorrect encryption key"); 
    } 
    else 
    { 
        trace("Error message:", event.error.message); 
        trace("Details:", event.error.details); 
    } 
} 

以下示例介绍以同步执行模式打开加密数据库。为了简单起见,此示例中的加密密钥在应用程序代码中采用硬编码形式。但是,由于此方法不安全,强烈建议不要使用此方法。

import flash.data.SQLConnection; 
import flash.data.SQLMode; 
import flash.filesystem.File; 
     
var conn:SQLConnection = new SQLConnection(); 
var dbFile:File = File.applicationStorageDirectory.resolvePath("DBSample.db"); 
     
var encryptionKey:ByteArray = new ByteArray(); 
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure! 
     
try 
{ 
    conn.open(dbFile, SQLMode.UPDATE, false, 1024, encryptionKey); 
    trace("the database was created successfully"); 
} 
catch (error:SQLError) 
{ 
    if (error.errorID == 3138) 
    { 
        trace("Incorrect encryption key"); 
    } 
    else 
    { 
        trace("Error message:", error.message); 
        trace("Details:", error.details); 
    } 
} 

有关介绍生成加密密钥的建议方式的示例,请参阅 示例:生成和使用加密密钥

更改数据库的加密密钥

数据库加密后,可以在以后更改数据库的加密密钥。要更改数据库的加密密钥,请首先创建一个 SQLConnection 实例并调用其 open() openAsync() 方法,从而打开到数据库的连接。连接数据库后,调用 reencrypt() 方法,并传递新的加密密钥作为参数。

与大多数数据库操作类似的是, reencrypt() 方法的行为根据数据库连接使用同步还是异步执行模式而有所不同。如果使用 open() 方法连接到数据库,则 reencrypt() 操作会同步运行。操作完成后,继续执行下一行代码:

var newKey:ByteArray = new ByteArray(); 
// ... generate the new key and store it in newKey 
conn.reencrypt(newKey);

另一方面,如果使用 openAsync() 方法打开数据库连接,则 reencrypt() 操作为异步方式。调用 reencrypt() 将开始重新加密的过程。操作完成后,SQLConnection 对象调度一个 reencrypt 事件。使用事件侦听器确定重新加密完成的时间:

var newKey:ByteArray = new ByteArray(); 
// ... generate the new key and store it in newKey 
     
conn.addEventListener(SQLEvent.REENCRYPT, reencryptHandler); 
     
conn.reencrypt(newKey); 
     
function reencryptHandler(event:SQLEvent):void 
{ 
    // save the fact that the key changed 
}

reencrypt() 操作在其自身的事务中运行。如果操作中断或失败(例如,如果在操作完成之前关闭应用程序),则事务将回滚。在这种情况下,原始的加密密钥仍为数据库的加密密钥。

reencrypt() 方法不能用于解除对数据库的加密。向 reencrypt() 方法传递 null 值或非 16 字节 ByteArray 的加密密钥会导致错误。

对数据库使用加密的注意事项

加密数据库的用途 部分介绍了需要使用加密数据库的几种情况。显然,不同应用程序的使用情况(包括以上这些情况和其他情况)具有不同的隐私要求。如何安排加密在应用程序中的用途,对于控制数据库的数据私密程度起着重要作用。例如,如果使用加密数据库来保持个人数据的私密性(甚至针对同一计算机的其他用户),则每个用户的数据库都需要有自己的加密密钥。为尽可能的安全起见,应用程序可以根据用户输入的密码生成密钥。加密密钥以密码为基础可以确保,即使其他人可以在计算机上模拟用户的帐户,也无法访问数据。换个角度来看隐私问题,假设您希望数据库文件可以由您的应用程序的任何用户读取,但不能由其他应用程序读取。在这种情况下,每个已安装的应用程序副本都需要具有访问共享加密密钥的权限。

可以根据希望应用程序数据所达到的隐私等级来设计应用程序,尤其是用于生成加密密钥的方法。以下列表为各种级别的数据隐私提供了设计建议:

  • 若要使任何计算机上有权访问应用程序的任何用户都可以访问数据库,请使用一个密钥,该密钥对于应用程序的所有实例都可用。例如,应用程序首次运行时,可以使用一个安全协议(如 SSL)从服务器下载共享的加密密钥。然后可以将密钥保存在加密本地存储区中,以供将来使用。作为替代方法,可以按计算机上的每个用户对数据进行加密,然后将数据与远程数据存储区(如服务器)同步,以使数据可移植。

  • 若要使任何计算机上的单个用户都可以访问数据库,请根据用户的保密事项(如密码)生成加密密钥。尤其是不要使用任何与特定计算机关联的值(如存储在加密本地存储区中的值)生成密钥。作为替代方法,可以按计算机上的每个用户对数据进行加密,然后将数据与远程数据存储区(如服务器)同步,以使数据可移植。

  • 若要使单个计算机上的单个人可以访问数据库,请根据密码和所生成的 salt 来生成密钥。有关此方法的示例,请参阅 示例:生成和使用加密密钥

设计应用程序使用加密数据库时,还有一些安全注意事项务必要牢记,具体如下所示:

  • 系统的安全性取决于其最薄弱环节的安全性。如果使用用户输入的密码生成加密密钥,则请考虑对密码施加最小长度和复杂性的限制。只使用基本字符的短密码很快就会被猜中。

  • AIR 应用程序的源代码以纯文本形式(对于 HTML 内容)或易于反编译的二进制格式(对于 SWF 内容)存储在用户的计算机上。由于源代码可访问,因此有两点要牢记:

    • 切勿在源代码中对加密密钥进行硬编码

    • 始终假设用于生成加密密钥的方法(如随机字符生成器或特定的哈希算法)很容易就会被攻击者破解

  • AIR 数据库加密使用高级加密标准 (AES) 及 Counter with CBC-MAC (CCM) 模式。这种加密密码需要将用户输入的密钥与 salt 值组合在一起才安全。有关此方法的示例,请参阅 示例:生成和使用加密密钥

  • 如要加密数据库,则数据库引擎所使用的所有磁盘文件与该数据库一起都要进行加密。 但是,在事务过程中,数据库引擎会在内存中的缓存中临时保留一些数据,以提高读写性能。驻留在内存中的任何数据都不加密。如果攻击者可以访问 AIR 应用程序所使用的内存(例如通过使用调试器),则数据库中当前公开且未加密的数据即可供其使用。

示例:生成和使用加密密钥

此示例应用程序介绍生成加密密钥的一种方法。此应用程序旨在为用户的数据提供最高级别的隐私和安全。保障私有数据安全的一个重要原则是:在应用程序每次连接到数据库时都要求用户输入密码。 因此,正如此例所示,需要此种保密级别的应用程序在任何时候都不应直接存储数据库加密密钥。

应用程序由两部分组成:生成加密密钥的 ActionScript 类(EncryptionKeyGenerator 类),以及介绍如何使用该类的基础用户界面。 有关完整的源代码,请参阅 用于生成和使用加密密钥的完整示例代码

使用 EncryptionKeyGenerator 类获得安全加密密钥

不需要了解 EncryptionKeyGenerator 类的工作方式的详情即可在您的应用程序中使用它。如果您有兴趣详细了解此类是如何为数据库生成加密密钥的,请参阅 了解 EncryptionKeyGenerator 类

请按照以下步骤在应用程序中使用 EncryptionKeyGenerator 类:

  1. 将 EncryptionKeyGenerator 类作为源代码或编译的 SWC 进行下载。EncryptionKeyGenerator 类包括在开源 ActionScript 3.0 核心库 (as3corelib) 项目中。可以下载 包括源代码和文档的 as3corelib 包 。还可以从项目页面下载 SWC 或源代码文件。

  2. 将 EncryptionKeyGenerator 类的源代码(即 as3corelib SWC)放置在应用程序源代码可以找到的地方。

  3. 在应用程序源代码中,为 EncryptionKeyGenerator 类添加一条 import 语句。

    import com.adobe.air.crypto.EncryptionKeyGenerator;
  4. 在创建数据库或打开数据库连接的代码位置之前,添加一段代码,以通过调用 EncryptionKeyGenerator() 构造函数来创建 EncryptionKeyGenerator 实例。

    var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
  5. 从用户处获取密码:

    var password:String = passwordInput.text; 
     
    if (!keyGenerator.validateStrongPassword(password)) 
    { 
        // display an error message 
        return; 
    }

    EncryptionKeyGenerator 实例使用此密码作为加密密钥的基础(下一步中介绍)。EncryptionKeyGenerator 实例对照特定的强密码验证要求测试该密码。如果验证失败,则发生错误。如示例代码所示,可以通过调用 EncryptionKeyGenerator 对象的 validateStrongPassword() 方法提前检查密码。这样可以确定密码是否符合强密码的最低要求,从而避免出错。

  6. 根据密码生成加密密钥:

    var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);

    getEncryptionKey() 方法生成并返回加密密钥(16 字节的 ByteArray)。然后即可使用该加密密钥创建新的加密数据库,或打开现有的加密数据库。

    getEncryptionKey() 方法有一个必需的参数,即第 5 步中获取的密码。

    注: 为使数据获得最大程度的安全性和保密性,应用程序必须在每次连接到数据库时都要求用户输入密码。请勿直接存储用户的密码或数据库加密密钥。这样做将面临安全风险。正如本例所示,应用程序在创建加密数据库时和以后连接到该数据库时,应该使用相同的技术根据密码派生加密密钥。

    getEncryptionKey() 方法还接受第二个(可选)参数,即 overrideSaltELSKey 。EncryptionKeyGenerator 创建一个随机值(称为 salt ),将其用作加密密钥的一部分。为了能够重新创建加密密钥,salt 值存储在 AIR 应用程序的加密本地存储区 (ELS) 中。默认情况下,EncryptionKeyGenerator 类使用特定的字符串作为 ELS 密钥。虽然可能性不大,但该密钥有可能与应用程序使用的另一个密钥发生冲突。您可能希望指定自己的 ELS 密钥,而不使用默认密钥。在这种情况下,请指定自定义密钥,方法是将它作为第二个 getEncryptionKey() 参数进行传递,如下所示:

    var customKey:String = "My custom ELS salt key"; 
    var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password, customKey);
  7. 创建或打开数据库

    通过由 getEncryptionKey() 方法返回的加密密钥,代码可以创建新的加密数据库,或尝试打开现有的加密数据库。在这两种情况下,都使用 SQLConnection 类的 open() openAsync() 方法,如 创建加密数据库 连接到加密数据库 中所述。

    在此示例中,应用程序设计为以异步执行模式打开数据库。代码设置适当的事件侦听器,调用 openAsync() 方法,并传递加密密钥作为最终参数:

    conn.addEventListener(SQLEvent.OPEN, openHandler); 
    conn.addEventListener(SQLErrorEvent.ERROR, openError); 
     
    conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);

    在侦听器方法中,代码取消了事件侦听器的注册。然后显示状态消息,表明是创建、打开了数据库,还是发生了错误。这些事件处理函数中,最值得注意的部分是 openError() 方法。在该方法中,有一个 if 语句检查数据库是否存在(意味着代码正在尝试连接到现有的数据库),以及错误 ID 是否与常量 EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID 匹配。如果这两个条件都符合,则可能表示用户输入的密码有误。(也有可能表示指定的文件根本不是数据库文件。)以下是用于检查错误 ID 的代码:

    if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID) 
    { 
        statusMsg.text = "Incorrect password!"; 
    } 
    else 
    { 
        statusMsg.text = "Error creating or opening database."; 
    }

    有关示例事件侦听器的完整代码,请参阅 用于生成和使用加密密钥的完整示例代码

用于生成和使用加密密钥的完整示例代码

以下是示例应用程序“生成和使用加密密钥”的完整代码。代码由两部分组成。

该示例使用 EncryptionKeyGenerator 类根据密码创建加密密钥。 EncryptionKeyGenerator 类包括在开源 ActionScript 3.0 核心库 (as3corelib) 项目中。可以下载 包括源代码和文档的 as3corelib 包 。还可以从项目页面下载 SWC 或源代码文件。

Flex 示例

应用程序 MXML 文件包含一个简单应用程序的源代码,该应用程序创建加密数据库或打开到加密数据库的连接:

<?xml version="1.0" encoding="utf-8"?> 
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init();"> 
    <mx:Script> 
        <![CDATA[ 
            import com.adobe.air.crypto.EncryptionKeyGenerator; 
             
            private const dbFileName:String = "encryptedDatabase.db"; 
             
            private var dbFile:File; 
            private var createNewDB:Boolean = true; 
            private var conn:SQLConnection; 
             
            // ------- Event handling ------- 
             
            private function init():void 
            { 
                conn = new SQLConnection(); 
                dbFile = File.applicationStorageDirectory.resolvePath(dbFileName); 
                if (dbFile.exists) 
                { 
                    createNewDB = false; 
                    instructions.text = "Enter your database password to open the encrypted database."; 
                    openButton.label = "Open Database"; 
                } 
            } 
             
            private function openConnection():void 
            { 
                var password:String = passwordInput.text; 
                 
                var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); 
                 
                if (password == null || password.length <= 0) 
                { 
                    statusMsg.text = "Please specify a password."; 
                    return; 
                } 
                 
                if (!keyGenerator.validateStrongPassword(password)) 
                { 
                    statusMsg.text = "The password must be 8-32 characters long. It must contain at least one lowercase letter, at least one uppercase letter, and at least one number or symbol."; 
                    return; 
                } 
                 
                passwordInput.text = ""; 
                passwordInput.enabled = false; 
                openButton.enabled = false; 
                 
                var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password); 
                 
                conn.addEventListener(SQLEvent.OPEN, openHandler); 
                conn.addEventListener(SQLErrorEvent.ERROR, openError); 
                  
                conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey); 
            } 
             
            private function openHandler(event:SQLEvent):void 
            { 
                conn.removeEventListener(SQLEvent.OPEN, openHandler); 
                conn.removeEventListener(SQLErrorEvent.ERROR, openError); 
                  
                statusMsg.setStyle("color", 0x009900); 
                if (createNewDB) 
                { 
                    statusMsg.text = "The encrypted database was created successfully."; 
                } 
                else 
                { 
                    statusMsg.text = "The encrypted database was opened successfully."; 
                } 
            } 
              
            private function openError(event:SQLErrorEvent):void 
            { 
                conn.removeEventListener(SQLEvent.OPEN, openHandler); 
                conn.removeEventListener(SQLErrorEvent.ERROR, openError); 
     
                if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID) 
                { 
                    statusMsg.text = "Incorrect password!"; 
                } 
                else 
                { 
                    statusMsg.text = "Error creating or opening database."; 
                } 
            } 
        ]]> 
    </mx:Script> 
    <mx:Text id="instructions" text="Enter a password to create an encrypted database. The next time you open the application, you will need to re-enter the password to open the database again." width="75%" height="65"/> 
    <mx:HBox> 
        <mx:TextInput id="passwordInput" displayAsPassword="true"/> 
        <mx:Button id="openButton" label="Create Database" click="openConnection();"/> 
    </mx:HBox> 
    <mx:Text id="statusMsg" color="#990000" width="75%"/> 
</mx:WindowedApplication>

Flash Professional 示例

应用程序 FLA 文件包含一个简单应用程序的源代码,该应用程序创建加密数据库或打开到加密数据库的连接。 FLA 文件有以下四个放置在舞台上的组件:

实例名称

组件类型

说明

instructions

标签

包含提供给用户的说明

passwordInput

文本输入

用户从中输入密码的输入字段

openButton

按钮

输入密码后用户单击的按钮

statusMsg

标签

显示状态(成功或失败)消息

在主时间轴的第 1 帧上的关键帧上定义此应用程序的代码。 以下是应用程序的代码:

import com.adobe.air.crypto.EncryptionKeyGenerator; 
     
const dbFileName:String = "encryptedDatabase.db"; 
     
var dbFile:File; 
var createNewDB:Boolean = true; 
var conn:SQLConnection; 
     
init(); 
     
// ------- Event handling ------- 
     
function init():void 
{ 
    passwordInput.displayAsPassword = true; 
    openButton.addEventListener(MouseEvent.CLICK, openConnection); 
    statusMsg.setStyle("textFormat", new TextFormat(null, null, 0x990000)); 
     
    conn = new SQLConnection(); 
    dbFile = File.applicationStorageDirectory.resolvePath(dbFileName); 
     
    if (dbFile.exists) 
    { 
        createNewDB = false; 
        instructions.text = "Enter your database password to open the encrypted database."; 
        openButton.label = "Open Database"; 
    } 
    else 
    { 
        instructions.text = "Enter a password to create an encrypted database. The next time you open the application, you will need to re-enter the password to open the database again."; 
        openButton.label = "Create Database"; 
    } 
} 
     
function openConnection(event:MouseEvent):void 
{ 
    var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); 
     
    var password:String = passwordInput.text; 
     
    if (password == null || password.length <= 0) 
    { 
        statusMsg.text = "Please specify a password."; 
        return; 
    } 
     
    if (!keyGenerator.validateStrongPassword(password)) 
    { 
        statusMsg.text = "The password must be 8-32 characters long. It must contain at least one lowercase letter, at least one uppercase letter, and at least one number or symbol."; 
        return; 
    } 
     
    passwordInput.text = ""; 
    passwordInput.enabled = false; 
    openButton.enabled = false; 
     
    var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password); 
     
    conn.addEventListener(SQLEvent.OPEN, openHandler); 
    conn.addEventListener(SQLErrorEvent.ERROR, openError); 
     
    conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey); 
} 
     
function openHandler(event:SQLEvent):void 
{ 
    conn.removeEventListener(SQLEvent.OPEN, openHandler); 
    conn.removeEventListener(SQLErrorEvent.ERROR, openError); 
     
    statusMsg.setStyle("textFormat", new TextFormat(null, null, 0x009900)); 
    if (createNewDB) 
    { 
        statusMsg.text = "The encrypted database was created successfully."; 
    } 
    else 
    { 
        statusMsg.text = "The encrypted database was opened successfully."; 
    } 
} 
 
function openError(event:SQLErrorEvent):void 
{ 
    conn.removeEventListener(SQLEvent.OPEN, openHandler); 
    conn.removeEventListener(SQLErrorEvent.ERROR, openError); 
     
    if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID) 
    { 
        statusMsg.text = "Incorrect password!"; 
    } 
    else 
    { 
        statusMsg.text = "Error creating or opening database."; 
    } 
}

了解 EncryptionKeyGenerator 类

不必了解 EncryptionKeyGenerator 类的内部工作机制,也可以使用它为应用程序数据库创建安全加密密钥。 使用 EncryptionKeyGenerator 类获得安全加密密钥 中介绍了使用该类的过程。但是,您会发现非常值得了解该类使用的技术。例如,对于需要不同数据保密级别的场合,可能要改编该类或加入它的某些技术。

EncryptionKeyGenerator 类包括在开源 ActionScript 3.0 核心库 (as3corelib) 项目中。可以下载 包括源代码和文档的 as3corelib 包 。还可以在项目站点查看源代码,或进行下载以按照介绍进行操作。

代码创建 EncryptionKeyGenerator 实例并调用其 getEncryptionKey() 方法时,将采取若干步骤来确保只有正当的用户才能访问数据。此过程与创建数据库之前,根据用户输入的密码生成加密密钥的过程相同,亦与重新创建加密密钥,以打开数据库的过程相同。

获取并验证强密码

代码调用 getEncryptionKey() 方法时,将传入密码作为参数。密码用作加密密钥的基础。此设计使用只有用户知道的一条信息,从而确保只有知道密码的用户才能访问数据库中的数据。即使攻击者可以访问用户在计算机上的帐户,在不知道密码的情况下也无法进入数据库。为尽可能安全起见,应用程序从不存储密码。

应用程序的代码创建 EncryptionKeyGenerator 实例并调用其 getEncryptionKey() 方法,将用户输入的密码作为参数(此示例中为 password 变量)传递:

var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); 
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);

调用 getEncryptionKey() 方法后,EncryptionKeyGenerator 类所采取的第一步是检查用户输入的密码,以确保其符合密码强度的要求。EncryptionKeyGenerator 类要求密码长度为 8 - 32 个字符。密码必须包含大小写字母的混合形式,而且至少包括一个数字或符号字符。

检查此模式的正则表达式被定义为一个常量,名为 STRONG_PASSWORD_PATTERN

private static const STRONG_PASSWORD_PATTERN:RegExp = /(?=^.{8,32}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/;

检查密码的代码位于 EncryptionKeyGenerator 类的 validateStrongPassword() 方法中。代码如下所示:

public function vaidateStrongPassword(password:String):Boolean 
{ 
    if (password == null || password.length <= 0) 
    { 
        return false; 
    } 
     
    return STRONG_PASSWORD_PATTERN.test(password)) 
}

在内部, getEncryptionKey() 方法调用 EncryptionKeyGenerator 类的 validateStrongPassword() 方法,并且密码无效时会引发异常。 validateStrongPassword() 方法是一个公共方法,这样应用程序代码无需调用 getEncryptionKey() 方法即可进行密码检查,从而避免导致错误。

将密码扩展到 256 位

在过程的后期,密码的长度必须为 256 位。代码并不要求每个用户输入长度刚好为 256 位(32 个字符)的密码,而是通过重复密码字符来创建更长的密码。

getEncryptionKey() 方法调用 concatenatePassword() 方法以执行创建长密码的任务。

var concatenatedPassword:String = concatenatePassword(password);

以下是 concatenatePassword() 方法的代码:

private function concatenatePassword(pwd:String):String 
{ 
    var len:int = pwd.length; 
    var targetLength:int = 32; 
     
    if (len == targetLength) 
    { 
        return pwd; 
    } 
     
    var repetitions:int = Math.floor(targetLength / len); 
    var excess:int = targetLength % len; 
     
    var result:String = ""; 
     
    for (var i:uint = 0; i < repetitions; i++) 
    { 
        result += pwd; 
    } 
     
    result += pwd.substr(0, excess); 
     
    return result; 
}

如果密码长度小于 256 位,则代码将密码与密码自身连接在一起,使其达到 256 位。 如果不能刚好达到这一长度,则缩短最后一次重复的内容,以便刚好得到 256 位。

生成或检索 256 位 salt 值

下一步是获得 256 位 salt 值,后面的一个步骤中会将此值与密码组合在一起。 salt 是向用户输入的值添加或与之组合在一起而形成密码的一个随机值。结合使用 salt 和密码确保了即使用户选择真实单词或常用词汇作为密码,系统所使用的“密码加 salt”组合也是一个随机值。这有助于防御字典攻击,攻击者在字典攻击中使用单词列表尝试猜出密码。 此外,通过生成 salt 值并将其存储在加密本地存储区中,该值与数据库文件所在计算机上的用户帐户相关联。

如果应用程序是第一次调用 getEncryptionKey() 方法,则代码将创建一个随机的 256 位 salt 值。否则,代码从加密本地存储区加载 salt 值。

salt 存储在名为 salt 的变量中。此代码确定它是否已通过从加密本地存储区加载 salt 而创建了 salt:

var salt:ByteArray = EncryptedLocalStore.getItem(saltKey); 
if (salt == null) 
{ 
    salt = makeSalt(); 
    EncryptedLocalStore.setItem(saltKey, salt); 
}

如果代码要创建新的 salt 值, makeSalt() 方法会生成一个 256 位的随机值。由于该值最终存储在加密本地存储区中,因此生成该值时将其作为 ByteArray 对象。 makeSalt() 方法使用 Math.random() 方法随机生成该值。 Math.random() 方法无法一次生成 256 位。作为替代,代码使用循环,调用 Math.random() 八次。每次都生成一个 0 到 4294967295(最大的 uint 值)之间的 uint 值。使用 uint 值是出于方便的目的,因为 uint 刚好为 32 位。通过向 ByteArray 中写入八个 uint 值,即生成一个 256 位值。以下是 makeSalt() 方法的代码:

private function makeSalt():ByteArray 
{ 
    var result:ByteArray = new ByteArray; 
     
    for (var i:uint = 0; i < 8; i++) 
    { 
        result.writeUnsignedInt(Math.round(Math.random() * uint.MAX_VALUE)); 
    } 
     
    return result; 
}

代码将 salt 保存到加密本地存储区 (ELS) 或从 ELS 检索 salt 时,代码需要将 salt 保存在其下的 String 密钥。 不知道密钥即无法检索 salt 值。 在这种情况下,无法在每次重新打开数据库时重新创建加密密钥。 默认情况下,EncryptionKeyGenerator 使用预定义的 ELS 密钥,该密钥在常量 SALT_ELS_KEY 中定义。如果不使用默认密钥的话,应用程序代码还可以指定一个 ELS 密钥,供调用 getEncryptionKey() 方法的过程中使用。默认的或由应用程序指定的 salt ELS 密钥存储在名为 saltKey 的变量中。该变量用于调用 EncryptedLocalStore.setItem() EncryptedLocalStore.getItem() ,如上所示。

使用 XOR 运算符组合 256 位密码和 salt

代码现在拥有一个 256 位密码和一个 256 位 salt 值。 然后,代码使用按位 XOR 运算将 salt 和连接而成的密码组合为一个值。 实际上,这种方法创建的 256 位密码由整个可用字符范围内的字符组成。 即使实际输入的密码主要由字母数字字符组成也是如此。 如此提高随机性的优点在于:无须用户输入长而复杂的密码,即可使可能密码的集合变得非常大。

XOR 运算的结果存储在变量 unhashedKey 中。对这两个值执行按位 XOR 的实际过程发生在 xorBytes() 方法中:

var unhashedKey:ByteArray = xorBytes(concatenatedPassword, salt);

按位 XOR 运算符 ( ^ ) 采用两个 uint 值,并返回一个 uint 值。(uint 值包含 32 位。)作为参数传递给 xorBytes() 方法的输入值为 String(密码)和 ByteArray (salt)。因此,代码使用循环,一次从每个输入提取 32 位,以使用 XOR 运算符进行组合。

private function xorBytes(passwordString:String, salt:ByteArray):ByteArray 
{ 
    var result:ByteArray = new ByteArray(); 
     
    for (var i:uint = 0; i < 32; i += 4) 
    { 
        // ... 
    } 
     
    return result; 
}

在循环中,首先从 passwordString 参数提取 32 位(4 个字节)。提取这些位之后,将在一个包含两部分的过程中将这些位转换为 uint ( o1 )。首先, charCodeAt() 方法获取每个字符的数值。接下来,使用按位左移运算符 ( << ) 将该值移动到 uint 中的适当位置,并将所移动的值添加到 o1 。例如,通过使用按位左移运算符 ( << ) 将第一个字符 ( i ) 左移 24 位,并将该值赋给 o1 ,使第一个字符成为第一个 8 位。通过将第二个字符 (i + 1 ) 的值左移 16 位,并将结果添加到 o1 ,使第二个字符成为第二个 8 位。如法炮制,添加第三个和第四个字符的值。

        // ... 
         
        // Extract 4 bytes from the password string and convert to a uint 
        var o1:uint = passwordString.charCodeAt(i) << 24; 
        o1 += passwordString.charCodeAt(i + 1) << 16; 
        o1 += passwordString.charCodeAt(i + 2) << 8; 
        o1 += passwordString.charCodeAt(i + 3); 
         
        // ...

变量 o1 现在包含 passwordString 参数中的 32 位。接下来,通过调用其 readUnsignedInt() 方法,从 salt 参数提取 32 位。这 32 位存储在 uint 变量 o2 中。

        // ... 
         
        salt.position = i; 
        var o2:uint = salt.readUnsignedInt(); 
         
        // ...

最后,使用 XOR 运算符将这两个 32 位 (uint) 值组合在一起,并将结果写入名为 result 的 ByteArray 中。

        // ... 
         
        var xor:uint = o1 ^ o2; 
        result.writeUnsignedInt(xor); 
        // ...

循环完成后,即返回包含 XOR 结果的 ByteArray。

        // ... 
    } 
     
    return result; 
}

对密钥进行哈希处理

将连接而成的密码与 salt 组合在一起后,下一步就是进一步加强对此值的保护,具体而言就是使用 SHA-256 哈希算法对此值进行哈希处理。 对值进行哈希处理使攻击者更难以对其进行反向工程。

此时,代码拥有一个名为 unhashedKey 的 ByteArray,其中包含与 salt 组合在一起的连接而成的密码。ActionScript 3.0 核心库 (as3corelib) 项目在 com.adobe.crypto 包中含有一个 SHA256 类。 SHA256.hashBytes() 方法对 ByteArray 执行 SHA-256 哈希处理,并返回一个包含 256 位哈希值结果的 String(以十六进制数表示)。EncryptionKeyGenerator 类使用 SHA256 类对密钥进行哈希处理:

var hashedKey:String = SHA256.hashBytes(unhashedKey);

从哈希值提取加密密钥

加密密钥必须为刚好 16 个字节(128 位)长的 ByteArray。SHA-256 哈希算法的结果的长度始终为 256 位。因此,最后一步是从哈希处理的结果中选择 128 位作为实际的加密密钥。

在 EncryptionKeyGenerator 类中,代码通过调用 generateEncryptionKey() 方法,将密钥缩短为 128 位。代码随后返回该方法的结果作为 getEncryptionKey() 方法的结果:

var encryptionKey:ByteArray = generateEncryptionKey(hashedKey); 
return encryptionKey;

并不一定要使用前 128 位作为加密密钥。可以选择从某任意点开始的一系列位,可以每隔一位选择一位,或使用某些其他方式来选择位。 重要的是代码选择 128 个不同的位,并且每次都使用相同的 128 位。

本例中, generateEncryptionKey() 方法使用从第 18 个字节开始的一些位作为加密密钥。如前所述,SHA256 类返回一个包含 256 位哈希值的 String(以十六进制数表示)。单块 128 位的字节数过多,无法一次添加到 ByteArray。因此,代码使用 for 循环,从十六进制的 String 提取字符,将这些字符转换为实际的数字值,并将其添加到 ByteArray。SHA-256 结果 String 的长度为 64 个字符。 128 位的范围等于 String 中的 32 个字符,因此每个字符代表 4 位。 可以添加到 ByteArray 的最小数据增量为一个字节(8 位),等于 hash String 中的两个字符。因此,循环的计数范围是 0 到 31(32 个字符),增量为 2 个字符。

在循环内,代码首先确定当前字符对的起始位置。由于所需的范围从索引位置 17(第 18 个字节)处的字符开始,因此将当前迭代器的值 ( i ) 加上 17 后赋给 position 变量。代码使用 String 对象的 substr() 方法提取当前位置的两个字符。将这些字符存储在变量 hex 中。接下来,代码使用 parseInt() 方法将 hex String 转换为十进制整数值。将该值存储在 int 变量 byte 中。最后,代码使用其 writeByte() 方法将 byte 中的值添加到 result ByteArray 中。循环完成时, result ByteArray 包含 16 个字节,并已准备就绪,可以用作数据库加密密钥。

private function generateEncryptionKey(hash:String):ByteArray 
{ 
    var result:ByteArray = new ByteArray(); 
     
    for (var i:uint = 0; i < 32; i += 2) 
    { 
        var position:uint = i + 17; 
        var hex:String = hash.substr(position, 2); 
        var byte:int = parseInt(hex, 16); 
        result.writeByte(byte); 
    } 
     
    return result; 
}