為 SQL 資料庫加密

Adobe AIR 1.5 以及更新的版本

所有的 Adobe AIR 應用程式都使用相同的本機資料庫引擎。因此,所有 AIR 應用程式都可以對未加密的資料庫檔案進行連線、讀取和寫入。從 Adobe AIR 1.5 開始,AIR 便納入了建立和連線未加密資料庫檔案的功能。當您使用加密資料庫時,應用程式如果想連線至資料庫,就必須提供正確的加密金鑰。如果提供的加密金鑰有誤 (或沒有提供金鑰),應用程式就無法連線至資料庫。因此,應用程式就無法讀取資料庫的資料,也無法將資料寫入或變更。

若要使用加密資料庫,您必須將資料庫建立為加密資料庫。若有現存的加密資料庫,您就可以開啟對該資料庫的連線。您也可以變更加密資料庫的加密金鑰。除了建立和連線的方法不同以外,加密資料庫的使用方法都與非加密資料庫一樣,尤其是 SQL 陳述式的執行方法,不論資料庫是否加密皆相同。

加密資料庫的用法

每當您想針對資料庫所儲存的資料進行存取限制時,加密就是一個很有用的方法。Adobe AIR 的資料庫加密功能有多項用途,下列是幾個您可以使用加密資料庫的例子:

  • 從伺服器下載應用程式私人資料的唯讀快取。

  • 私人資料的本機應用程式儲存區與伺服器進行同步 (資料可傳送至伺服器亦可從伺服器載入)。

  • 應用程式所建立和編輯的文件採用加密檔案格式。這些檔案可供單一使用者私人使用,也可以指定讓應用程式的所有使用者共用。

  • 本機資料儲存區其它必須讓資料保持私密的用途 (例如本機 SQL 資料庫的用法所描述的幾個用途),避免所有具電腦或資料庫檔案存取權的人員都來存取資料。

若能瞭解自己為何需要使用加密資料庫,便可輕易判斷應該如何架構應用程式,尤其是因為這會影響應用程式將如何為資料庫建立、取得或儲存加密金鑰。如需關於這些考量事項的詳細資訊,請參閱為資料庫加密時的考量事項

除了加密資料庫以外,另外還有一種可以讓機密資料保持隱私的機制,就是加密本機儲存區。使用加密本機儲存區時,您將使用 String 金鑰來儲存單一的 ByteArray 值。只有儲存該值的 AIR 應用程式可以加以存取,而且只能在儲存該值的電腦上進行存取。如果使用加密本機儲存區,就不需要建立自己的加密金鑰。基於這些原因,加密本機儲存區最適合儲存單一值或是可在 ByteArray 中輕易完成編碼的單一組值。加密資料庫最適合較大型的資料集,此類資料集需要結構性資料儲存和查詢。如需有關使用加密本機儲存區的詳細資訊,請參閱加密本機儲存

建立加密資料庫

若要使用加密資料庫,建立資料庫檔案時就必須將其加密。如果資料庫在建立時沒有加密,之後就不能再進行加密。同樣地,已加密的資料庫也不能再解密。如果有需要,您可以變更加密資料庫的加密金鑰。如需詳細資訊,請參閱變更資料庫的加密金鑰。如果您的現有資料庫沒有加密,而您想使用資料庫加密,則可以建立一個新的加密資料庫,然後將現有的資料表結構和資料複製到新資料庫中。

加密資料庫與非加密資料庫的建立方法幾乎一樣,如建立資料庫一節所述。您可以先建立一個 SQLConnection 實體來代表對資料庫的連線,然後呼叫 SQLConnection 物件的 open() 方法或 openAsync() 方法為資料庫位置指定一個尚未存在的檔案,藉此方法建立資料庫。建立加密資料庫時的唯一差別就是,您需要提供 encryptionKey 參數的值 (也就是 open() 方法的第五個參數及 openAsync() 方法的第六個參數)。

有效的 encryptionKey 參數值就是一個剛好 16 位元組的 ByteArray 物件。http://help.adobe.com/zh_TW/Flash/CS5/AS3LR/flash/utils/ByteArray.html

以下範例示範如何建立加密的資料庫。為求簡化,這些範例中的加密金鑰在應用程式碼中使用硬式編碼。不過,我們非常不建議您使用這項技巧,因為很不安全。

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 例外。http://help.adobe.com/zh_TW/Flash/CS5/AS3LR/flash/errors/SQLError.html對於 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 資料庫加密會對 CBC-MAC (CCM) 模式的計數器使用「進階加密標準」(AES)。這個加密密碼需要一組由使用者輸入的金鑰來與 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 類別會使用特定的 String 做為 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

Label

包含提供給使用者的指示

passwordInput

TextInput

使用者在當中輸入密碼的輸入欄位

openButton

Button

使用者在輸入密碼後所按的按鈕

statusMsg

Label

顯示狀態 (成功或失敗) 訊息

應用程式的程式碼會定義在主時間軸中影格 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,判斷是否已經建立該值:

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() 八次,而每一次都會產生一個隨機的 uint 值,介於 0 和 4294967295 (最大的 uint 值) 之間。使用 uint 值是為了方便,因為 uint 剛好使用 32 位元。只要將八個 uint 值寫入 ByteArray,就能夠產生一個 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。舉例來說,當使用位元左移運算子 (<<) 往左移 24 位元後再將該值附加給 o1 時,第一個字元 (i) 就會變成第一組的 8 位元。當值往左移 16 位元後將其附加至 o1,第二個字元 (i + 1) 就會變成第二組的 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 位元。接著,呼叫 salt 參數的 readUnsignedInt() 方法,從參數中擷取出 32 位元。32 位元會儲存在 o2 這個 uint 變數中。

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

最後,這兩個 32 位元 (uint) 值會以 XOR 運算子結合,而結果將寫入一個名為 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 雜湊,然後傳回一個 String,其中含有一個十六進位數字形式的 256 位元雜湊結果。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 類別會傳回一個 String,其中含有一個十六進位數字形式的 256 位元雜湊結果。一個區塊便有 128 位元,因為有太多位元組,所以無法一次新增至 ByteArray。因此,程式碼使用 for 迴圈來從這個十六進位的 String 中擷取字元,然後再將那些字元轉換為真正的數值,再將數值新增至 ByteArray。SHA-256 結果 String 的長度為 64 個字元。128 位元的範圍等於 String 中的 32 個字元,而每個字元代表 4 個位元。可讓您新增至 ByteArray 的最小增量是一個位元組 (8 位元),等於 hash String 中的兩個字元。因此,迴圈的計數將從 0 至 31 (32 個字元),每次的增量為 2 個字元。

在迴圈中,程式碼會先找出當前這一對字元的開始位置。因為想挑出的範圍開始於索引位置 17 (也就是第 18 個位元組) 的字元,所以 position 變數的設定就是目前的迴圈指標值 (i) 加 17。程式碼會使用 String 物件的 substr() 方法來從目前的位置擷取出兩個字元。那些字元會儲存在 hex 變數中。然後,程式碼會使用 parseInt() 方法將 hex String 轉換為十進位整數,然後將該值儲存在 byte 這個 int 變數中。最後,程式碼會使用 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; 
}