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() メソッドの第 5 パラメーターであり、openAsync() メソッドの第 6 パラメーター)の値を指定することです。

有効な encryptionKey パラメーターの値は、16 バイトを格納している ByteArray オブジェクトです。http://help.adobe.com/ja_JP/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() メソッドの第 5 パラメーターであり、openAsync() メソッドの第 6 パラメーター)の適切な値を指定することです。

指定された暗号化キーが正しくない場合はエラーが発生します。open() メソッドの場合は、SQLError 例外がスローされます。http://help.adobe.com/ja_JP/Flash/CS5/AS3LR/flash/errors/SQLError.htmlopenAsync() メソッドの場合は、SQLConnection オブジェクトによって SQLErrorEvent が送出されます。これには、SQLError オブジェクトの error プロパティが格納されています。いずれの場合も、例外によって生成された SQLError オブジェクトの errorID プロパティの値は 3138 になります。このエラー ID は、「File opened is not a database file」というエラーメッセージに対応します。

暗号化されたデータベースを非同期実行モードで開く方法を次の例に示します。わかりやすくするために、この例では、暗号化キーをアプリケーションコードでハードコードしています。ただし、この方法は安全ではないため、使用しないでください。

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() メソッドを使用して、データベースの暗号化を解除することはできません。null 値または 16 バイト以外の ByteArray を reencrypt() メソッドに渡すと、エラーが発生します。

データベースで暗号化を使用する場合の考慮事項

暗号化されたデータベースの使用では、暗号化されたデータベースの使用が適している例をいくつか示します。機密性の要件は、アプリケーションの使用例(ここに示す例および他の例を含む)によって異なります。アプリケーションでの暗号化の使用をどのように設計するかが、データベースのデータの機密性を制御する上で重要な役割を担います。例えば、暗号化されたデータベースを使用して、同じマシンを使用している他のユーザーに対しても個人情報を公開しないようにするには、各ユーザーのデータベースで独自の暗号化キーが必要になります。セキュリティを最大限に高めるために、ユーザーが入力したパスワードに基づいてキーを生成することができます。パスワードに基づいて暗号化キーを生成すると、他のユーザーがマシンのユーザーのアカウントを偽装できたとしても、データにアクセスすることはできません。機密性について別の観点から考えてみましょう。特定のアプリケーションのすべてのユーザーがデータベースファイルを読み取ることができるようにする必要があるとします。ただし、他のアプリケーションのユーザーは、このデータベースファイルを読み取ることができないようにします。この場合、そのアプリケーションがインストールされている各環境から、共有の暗号化キーにアクセスできる必要があります。

アプリケーションデータに求められる機密性のレベルに応じて、アプリケーション(具体的には、暗号化キーの生成に使用する手法)を設計できます。様々なデータ機密性レベルにおけるデザイン上の推奨事項を次の一覧に示します。

  • 任意のマシンにインストールされている特定のアプリケーションにアクセスできるすべてのユーザーがデータベースにアクセスできるようにするには、アプリケーションのすべてのインスタンスで利用できる単一のキーを使用します。例えば、アプリケーションの初回実行時に、SSL などのセキュリティ保護されたプロトコルを使用して、サーバーから共有の暗号化キーをダウンロードできます。このキーを、暗号化されたローカルストアに保存して、次回以降も使用できるようにします。別の方法としては、マシンのユーザーごとにデータを暗号化し、サーバーなどのリモートデータストアとデータを同期させることで、データをポータブルにします。

  • 単一のユーザーがどのマシンからでもデータベースにアクセスできるようにするには、ユーザーの秘密(パスワードなど)から暗号化キーを生成します。特に、特定のコンピューターに関連付けられた値(例えば、暗号化されたローカルストアに保存されている値)は、キーの生成に使用しないでください。別の方法としては、マシンのユーザーごとにデータを暗号化し、サーバーなどのリモートデータストアとデータを同期させることで、データをポータブルにします。

  • 単一のマシンの単一のユーザーのみがデータベースにアクセスできるようにするには、パスワードおよび生成されたソルトからキーを生成します。この方法の例については、例:暗号化キーの生成と使用を参照してください。

暗号化されたデータベースを使用するアプリケーションを設計する場合に注意する必要がある、セキュリティ上の追加考慮事項を次に示します。

  • システムの安全性は、その最も弱い部分で決まります。ユーザーが入力したパスワードを使用して暗号化キーを生成する場合は、パスワードの最小の長さと複雑さについての制約を設けることを検討してください。基本文字のみで構成される短いパスワードは、簡単に推測できます。

  • AIR アプリケーションのソースコードは、プレーンテキスト(HTML コンテンツの場合)または簡単に逆コンパイルできるバイナリ形式(SWF コンテンツの場合)でユーザーのマシンに保存されます。ソースコードはアクセス可能なので、次の 2 つの点に注意する必要があります。

    • 暗号化キーをソースコード内にハードコードしないこと

    • 暗号化キーの生成に使用する技法(ランダム文字生成ツールや特定のハッシュアルゴリズムなど)は、攻撃者によって簡単に解読されることを前提とすること

  • AIR のデータベース暗号化では、AES(Advanced Encryption Standard)を CCM(Counter with CBC-MAC)モードで使用します。この暗号化では、暗号の安全性を高めるために、ユーザーが入力したキーをソルト値と組み合わせることが必要になります。この方法の例については、例:暗号化キーの生成と使用を参照してください。

  • データベースを暗号化することにした場合、そのデータベースに関連してデータベースエンジンが使用するすべてのディスクファイルが暗号化されます。ただし、データベースエンジンでは、トランザクションにおける読み取りと書き込みのパフォーマンスを改善するために、一部のデータを一時的にインメモリキャッシュに保持します。メモリに常駐するデータは暗号化されません。攻撃者がデバッガーなどを使用して、AIR アプリケーションによって使用されているメモリにアクセスできてしまうと、現在開いている、暗号化されていないデータベースのデータが入手可能になります。

例:暗号化キーの生成と使用

このアプリケーション例では、暗号化キーを生成する方法を 1 つ示します。このアプリケーションは、ユーザーのデータについて、最も高いレベルの機密性と安全性を提供するように設計されています。非公開データのセキュリティ保護に関する重要な側面の 1 つは、アプリケーションがデータベースに接続する際、ユーザーにパスワードの入力を毎回要求することです。したがって、この例が示すように、このレベルの機密性を要求するアプリケーションがデータベースの暗号化キーを直接保存しておくことは絶対に避けるべきです。

このアプリケーションは、暗号化キーを生成する ActionScript クラス(EncryptionKeyGenerator クラス)、および、そのクラスの使用方法を示す基本的なユーザーインターフェイスという 2 つの部分から構成されます。ソースコード全体については、暗号化キーの生成と使用のコード例全体を参照してください。

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() メソッドは必要なパラメーターを 1 つ持ちます。これは手順 5 で取得されたパスワードです。

    注意: アプリケーションでは、最も高いレベルのデータのセキュリティとプライバシーを維持するために、アプリケーションがデータベースに接続するたびにユーザーのパスワード入力を要求する必要があります。ユーザーのパスワードまたはデータベース暗号化キーを直接格納しないでください。セキュリティのリスクが高くなります。代わりに、この例に示すように、暗号化データベースの作成時とその接続時に、同じ方法を使用してパスワードから暗号化キーを抽出するようにします。

    getEncryptionKey() メソッドは、2 番目(オプション)のパラメーターである overrideSaltELSKey パラメーターも受け入れます。EncryptionKeyGenerator は、salt と呼ばれるランダムな値を作成します。これは、暗号化キーの一部として使用されます。暗号化キーを再作成できるようにするために、salt の値は AIR アプリケーションの暗号化されたローカルストア(ELS)に格納されます。デフォルトでは、EncryptionKeyGenerator クラスは特定の String を ELS キーとして使用します。まれに、アプリケーションが使用している他のキーと、このキーが競合する可能性があります。デフォルトのキーを使用する代わりに、独自の ELS キーを指定することもできます。その場合は、次のように、2 番目の 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."; 
    }

    この例のイベントリスナーのコード全体については、暗号化キーの生成と使用のコード例全体を参照してください。

暗号化キーの生成と使用のコード例全体

アプリケーション例「暗号化キーの生成と使用」のコード全体を次に示します。このコードは 2 つの部分から構成されます。

この例では、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 ファイルのステージ上には、次に示す 4 個のコンポーネントが配置されています。

インスタンス名

コンポーネントの種類

説明

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);

EncryptionKeyGenerator クラスでは、getEncryptionKey() メソッドが呼び出されると、まずユーザーの入力したパスワードがパスワード強度の要件を満たしているかチェックします。EncryptionKeyGenerator クラスでは、パスワードの長さを 8 ~ 32 文字に設定する必要があります。パスワードは大文字と小文字を組み合わせたもので、数字または記号が少なくとも 1 文字含まれている必要があります。

このパターンをチェックするための正規表現が、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 ビットのソルト値の生成と取得

次の手順では、256 ビットのソルト値を取得します。このソルト値は後の手順でパスワードに結合されます。ソルトは、ランダム値であり、ユーザーが入力した値に追加または結合されてパスワードを形成します。ソルトをパスワードと共に使用することにより、実際に存在する単語や一般的な言葉をユーザーがパスワードとして選択した場合でも、システムで使用されるパスワードとソルトの組み合わせはランダム値になります。このランダムな性質は、辞書攻撃(攻撃者が単語のリストを使用してパスワードを推測しようとする)からシステムを守るために役立ちます。また、ソルト値を生成し、暗号化されたローカルストアに保存することにより、データベースファイルが置かれているマシンのユーザーアカウントに関連付けられます。

アプリケーションが getEncryptionKey() メソッドを初めて呼び出す際には、このコードによってランダムな 256 ビットのソルト値が生成されます。それ以外の場合は、暗号化されたローカルストアからソルト値が読み込まれます。

ソルトは、salt という名前の変数に格納されます。このコードでは、暗号化されたローカルストアからソルト値を読み込もうとすることで、既に作成済みのソルトがあるかどうかを判断しています。

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

新しいソルト値を作成する場合は、makeSalt() メソッドで 256 ビットのランダムな値を生成します。この値は、最終的に、暗号化されたローカルストアに格納されるため、ByteArray オブジェクトとして生成されます。makeSalt() メソッドは、Math.random() メソッドを使用して、ランダムに値を生成します。Math.random() メソッドでは、256 ビットの値を一度に生成することはできません。代わりに、ループを使用して、Math.random() を 8 回呼び出しています。それぞれの呼び出しでは、0 ~ 4294967295(uint 値の最大値)のランダムな uint 値が生成されます。uint はちょうど 32 ビットであるため、ここでは便宜上 uint 値を使用しています。8 個の 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; 
}

暗号化されたローカルストア(ELS)にソルトを保存する際と、ELS からソルトを抽出する際には、そのソルトの保存に使用する String キーが必要です。このキーがわからないと、ソルトの抽出はできず、したがって、暗号化キーを再現してデータベースを開き直すこともできません。EncryptionKeyGenerator のデフォルトでは、あらかじめ定数 SALT_ELS_KEY として定義されている ELS キーを使用します。デフォルト以外のキーを使用する場合は、アプリケーションのコードが getEncryptionKey() メソッドを呼び出す際に、使用する ELS キーを指定できます。デフォルトまたはアプリケーションによって指定されたソルト ELS キーは、saltKey という変数に格納されます。この変数は、前に示したように、EncryptedLocalStore.setItem()EncryptedLocalStore.getItem() の呼び出しで使用されます。

XOR 演算子による 256 ビットのパスワードとソルトの結合

256 ビットのパスワードと 256 ビットのソルト値が用意できました。次は、ソルト値と連結パスワードとをビット XOR 演算で組み合わせ、1 つの値にします。これによって、文字データとして表現可能なあらゆる文字を含む可能性がある 256 ビットのパスワードを作成できます。ユーザーの入力するパスワードは主に英数字で構成されている場合がほとんどですが、そうであっても、ここで生成されるパスワードの性質は変わりません。ユーザーに長く複雑なパスワードの入力を要求しなくても、こうしてランダムな性質を付加すれば、パスワードの組み合わせの数を増やすことができます。

XOR 演算の結果は、変数 unhashedKey に格納されます。2 つの値に対してビット単位の XOR を実行する実際の処理は、xorBytes() メソッドで行われます。

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

ビット単位の XOR 演算子(^)は、2 つの uint 値を受け取って、1 つの uint 値を返します(1 つの uint 値は 32 個のビットを含みます)。引数として xorBytes() メソッドに渡される入力値は、String(パスワード)と ByteArray(ソルト)です。したがって、ループを使用して入力値から一度に 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 バイト)を抽出します。抽出したビット列を、2 段階のプロセスを経て 1 つの uint 値(o1)に変換します。最初に、charCodeAt() メソッドによって各文字の数値が取得されます。次に、ビット単位の左シフト演算子(<<)を使用して値を uint 内の適切な位置にシフトし、その値を o1 に加算します。例えば、最初の文字(i)は、ビット単位の左シフト演算子(<<)を使用して 24 ビット分だけ左にシフトされ、その値を o1 に代入することにより、最初の 8 ビットになります。2 番目の文字((i + 1)は、値を 16 ビット分だけ左にシフトし、その結果を o1 に代入することにより、2 番目の 8 ビットになります。同様に、3 番目と 4 番目の文字の値が追加されます。

        // ... 
         
        // 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); 
         
        // ...

以上で、変数 o1passwordString パラメーターから抽出した 32 ビットが格納されました。次に、salt パラメーターから 32 ビットが抽出されます。これは、readUnsignedInt() メソッドを呼び出すことにより行います。この 32 ビット列を、uint の変数 o2 に格納します。

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

最後に、XOR 演算子を使用して 2 つの 32 ビット(uint)値を結合し、結果を result という名前の ByteArray に書き込みます。

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

ループが終了したら、XOR の結果を格納している ByteArray が返されます。

        // ... 
    } 
     
    return result; 
}

キーのハッシング

連結したパスワードとソルト値とを組み合わせた後は、さらにセキュリティを強化するために、SHA-256 ハッシュアルゴリズムを使用してこの値をハッシュします。値をハッシュすることにより、攻撃者がリバースエンジニアリングを行うのが困難になります。

この時点で、連結したパスワードとソルトを組み合わせた結果の値が unhashedKey という ByteArray に格納されています。ActionScript 3.0 コアライブラリ(as3corelib)プロジェクトに、com.adobe.crypto パッケージに属する SHA256 というクラスがあり、このクラスの SHA256.hashBytes() メソッドは、ByteArray に対して SHA-256 ハッシュを実行し、256 ビットのハッシュ結果を 16 進数形式で格納した String を返します。EncryptionKeyGenerator クラスでは、この SHA256 クラスを使用してキーをハッシュします。

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

ハッシュからの暗号化キーの抽出

暗号化キーは、16 バイト(128 ビット)ちょうどの長さの ByteArray でなければなりません。SHA-256 ハッシュアルゴリズムの結果は常に 256 ビット長になります。したがって、最後の手順では、実際の暗号化キーとして使用される 128 ビット分をハッシュ結果から選択します。

EncryptionKeyGenerator クラスのコードで、キーを 128 ビットに短縮するために generateEncryptionKey() メソッドを呼び出します。さらに、このメソッドの結果を getEncryptionKey() メソッドの結果として返します。

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

最初の 128 ビットは、暗号化キーとして使用する必要がありません。その他任意の場所から始まるビット範囲や、1 つおきに抽出したビット列、または何か別の方法で選択したビットを使用することもできます。重要なのは、128 個の別個のビットを選択すること、および毎回同じ 128 個のビットを使用することです。

この例の generateEncryptionKey() メソッドでは、18 番目のバイトから始まる範囲を暗号化キーとして使用しています。前述のように、SHA256 クラスは、256 ビットのハッシュを 16 進数値として格納した String を返します。128 ビットの塊はバイト数が多すぎるため、一度に ByteArray に追加することができません。このため、コードでは、for ループを使用して 16 進数の String から文字を抽出し、実際の数値に変換してから、ByteArray に追加します。SHA-256 の結果として得られる String の長さは 64 文字です。128 ビットの範囲がこの String の 32 文字に相当し、各文字は 4 ビットを表します。ByteArray に追加できる最小のデータインクリメント単位は 1 バイト(8 ビット)です。これは、hash String の 2 文字に相当します。このため、loop では、2 文字分のインクリメントで 0 ~ 31(32 文字)がカウントされます。

ループ内では、最初に現在の文字ペアの開始位置を特定します。目的の範囲はインデックス位置 17(18 番目のバイト)で開始されるため、position 変数には、現在のイテレータ値(i)に 17 を足した値が代入されます。コードでは、String オブジェクトの substr() メソッドを使用して、現在の位置にある 2 文字を抽出します。この文字は変数 hex に格納されます。次に、parseInt() メソッドを使用して、hex String を 10 進数の整数値に変換します。変換された値は、int 型の変数 byte に格納されます。最後に、byte の値を、result ByteArray に writeByte() メソッドで追加します。ループが終了した時点で、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; 
}