2022年2月10日木曜日

暗号処理を使ったファイル交換アプリの作成

 Oracle Databaseに組み込みのRSAとAESの暗号処理を使って、ファイルを交換するアプリケーションを試作してみました。

以下の機能を実装します。

  1. ユーザーはそれぞれ自身のRSA公開キーを登録する。管理者が確認した上で登録した方が、セキュリティ面では有利だと思います。
  2. ファイルをアップロードする。アップロードする際にダウンロードを許可するユーザーを選択する。
  3. 許可されているファイルをダウンロードする。ダウンロードの際にセッション・キーを生成し、ファイルはセッション・キーを使ってAESで暗号化する。セッション・キーは登録済みのRSA公開キーで暗号化し、AESの初期ベクタと共に画面に表示する。
  4. 手元にダウンロードしたファイルは、初期ベクタ、RSAで暗号化されたセッション・キー、手元にあるRSAの秘密キーを使って復号する。(この処理はAPEXアプリではなく、openssl 3.0を使って行います)
また、RSA公開キーの登録、変更、無効化やファイルのアップロード、アクセス権の変更といったデータベースへの操作を履歴として保存するかわりに、すべての表をOracle Databaseに最近追加された機能であるImmutable Tableにして実装します。

アプリケーションの作成はAlways FreeのAutonomous Transaction Processingのインスタンスを使って行なっています。


DBMS_CRYPTOの実行権限の付与



APEXから作成したワークペースのスキーマには、デフォルトではパッケージDBMS_CRYPTOの実行権限が付与されていません。以下のGRANT文を実行し、APEXのワークスペース・スキーマに実行権限を付与します。

grant execute on dbms_crypto to ワークスペース・スキーマ名;

Autonomous Databaseの場合、データベース・アクション開発SQLより実行します。



表とビューの作成



最初にSQLワークショップユーティリティクイックSQLを使って、必要な表を定義します。クイックSQLの定義は以下になります。

クイックSQLの左側に貼り付け、SQLの生成SQLスクリプトを保存レビューおよび実行を順次実行します。


6つの表を作成しています。
  • EXC_CREDENTIALS - APEXアプリの認証スキームにて認証されたユーザー(置換文字列APP_USER)に紐づけて、RSA公開キーを保存します。同一のユーザーに複数の公開キーを登録できますが、ファイルのダウンロード時に使用される公開キーには、SUBMITTEDの日付が最新のものが選択されます。列SUBMITTEDが最新で列PUBLIC_KEYがNULLの場合、公開キーが未登録ということになり、このユーザーによるファイルのダウンロードの試みはすべて失敗します。
  • EXC_DOCUMENTS - アップロードしたファイルを列BODYに保存します。ファイル名は列BODY_FILENAME、アップロードしたユーザーは列SUBMITTED_BYに保存されます。
  • EXC_LINKS - DBMS_CRYPTO.RANDOMBYTESを呼び出して生成した外部キーと、アップロードしたファイルのIDを紐づけます。ファイルのダウンロードは、列EXTERNAL_KEYを引数にして行い、表EXC_DOCUMENTSのIDは指定しません。表EXC_DOCUMENTSのIDに対して複数の外部キーを登録できますが、有効な外部キーは最新のもの(SUBMITTEDが最新)のみです。外部キーが置き換えられると、以前のダウンロードURLからはダウンロード不可になります。
  • EXC_ACLS - ファイルをダウンロードする際に実施する暗号化処理で使用できるRSA公開キーを指定します。ACLはユーザーではなくユーザーに紐づいているRSA公開キーを割り当てているため、ユーザーがRSA公開キーを置き換えると、以前にダウンロード可能だったすべてのファイルのダウンロードはできなくなります。
  • EXC_DOWNLOADS - 成功したファイルのダウンロードの履歴を保存します。
  • EXC_ACCESS_LOG - 成功失敗に関わらず、ダウンロード要求の履歴を保存します。
生成されたDDLのレビュー画面が開くので、表をImmutable Tableとして作成する変更と、デフォルト値の指定の記述を修正します。
  • create tableの間にimmutableを挿入します。
  • create table文の末尾にno drop until 0 days idle no delete until 16 days after insertを挿入します。期間の指定(0 days16 daysの部分)はこの通りでなくても問題ないですが、0 daysの方の日付を0より大きくすると表のドロップができなくなります。そのため、アプリケーションが完成するまでは0 daysの指定が便利です。
  • 列SUBMITTED_BYのデフォルト値の指定より'(アポストロフィ)を取り除く。
以上の変更を6つのcreate table文すべてで実施します。


変更したDDLは以下になります。

スクリプトの変更後、実行をクリックし、表示された確認画面で即時実行をクリックします。すべてのDDLが成功していることを確認します。


Immutable Tableとして作成しているため、表EXC_CREDENTIALSおよびEXC_LINKSには無効となった過去のデータが含まれます。それぞれ有効な列のみが選択されるビューEXC_CREDENTIALS_VWEXC_LINKS_VWを定義します。

実行はSQLワークショップSQLコマンドより、それぞれのビューごとに実行するとよいでしょう。SQLスクリプトとして実行することもできます。


以上で、アプリケーションが必要としているデータベース・オブジェクトの準備は完了です。


初期アプリケーションの作成



アプリケーション・ビルダーより新規アプリケーションの作成を実行し、アプリケーション作成ウィザードを起動します。

アプリケーションの名前ファイル交換とし、アプリケーションの作成を実行します。他に特別な設定は行いません。


アプリケーションが作成されたら、最初にアプリケーションの別名を変更します。アプリケーション・プロパティの編集をクリックします。


アプリケーションの別名fileexchangeとし、変更の適用をクリックします。アプリケーションの別名は(簡易URLONの場合)URLに現れるため、英数字に限定するのが望ましいです。


ビルド・オプションとしてコメント・アウトを作成します。APEX 21.2からはデフォルトで作成されているため、APEX 21.2を使用している場合はこの作業は不要です。

共有コンポーネントビルド・オプションを開きます。


作成をクリックします。


ビルド・オプションコメント・アウトと入力します。ステータス除外エクスポートのデフォルト除外とします。コンポーネントのビルド・オプションにコメント・アウトを選択すると、そのコンポーネントは存在しないのと同じ扱いになります。


作成されたビルド・オプションが一覧されます。



以上でアプリケーションの準備は完了です。


RSA公開キーの登録



アプリケーションにユーザーがRSA公開キーを登録するページを作成します。

ページの作成を実行し、ページ作成ウィザードを起動します。


フォームを選択します。


公開キーは登録するだけなので、単体のフォームを選択します。


ページ番号とします。ページ名公開キーの登録ページ・モード標準を選択します。送信時にここにブランチは、ページの遷移が行われないように取り消してページに移動とします。

へ進みます。


ナビゲーションのプリファレンスとして新規ナビゲーション・メニュー・エントリの作成を選択します。新規ナビゲーション・メニュー・エントリはデフォルトで公開キーの登録になります。

へ進みます。


データ・ソースソース・タイプとしてSQL問合せを選択し、SQL SELECT文を入力に以下を記述します。

select
id
,username
,public_key
from exc_credentials
where username = :APP_USER


主キー列ID (Number)を選択します。作成をクリックします。


ページが作成されます。

このページから公開キーを登録するときに、常に列USERNAMEにサインインしたユーザー(APP_USER)が設定されるように、ページ・アイテムP2_USERNAMEを調整します。

ページ・アイテムP2_USERNAMEを選択し、タイプ非表示に変更します。


ページ・アイテムP2_USERNAME上でコンテキスト・メニューを開き、計算の作成を実行します。


作成された計算実行オプションポイントとして送信後を選択し、計算タイプアイテムアイテム名APP_USERを指定します。この計算により、P2_USERNAMEの値は常にAPP_USERの値(サインインしたユーザー名)となります。


リージョン・ボタンのCANCELDELETESAVEは使用しないので、これらのボタンを選択したのちコンテキスト・メニューを開いて削除を実行します。


公開キーを入力するページ・アイテムP2_PUBLIC_KEYの入力領域が小さいので、拡張します。

ページ・アイテムP2_PUBLIC_KEYを選択し、外観80高さ10に変更し、検証最大長4000文字にします。


以上でRSA公開キーを登録するページが出来上がりました。

ページを実行して、公開キーを登録してみます。

登録する公開キーはopensslの以下のコマンドを実行して生成します。

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem


登録された公開キーを確認します。SQLコマンドより以下のSELECT文を実行します。

select * from exc_credentials_vw;


登録した公開キーが検索されれば、動作確認は完了です。

公開キーを空白としてCreateを実行すると、登録済みの公開キーは無効になります。その場合、上記のSELECT文の結果はデータがみつかりませんになります。


ファイルの登録



交換するファイルを登録するページを作成します。

ページ作成ウィザードを起動し、フォームを選択します。


フォーム付きレポートを選択します。


レポート・タイプとして対話モード・レポートフォーム・ページ・モードとしてモーダル・ダイアログを選択します。それ以外は、レポート・ページ番号レポート・ページ名ファイル一覧フォーム・ページ番号フォーム・ページ名ファイル登録とします。ページ・アイテムの名称に影響があるので、ページ番号は3、4としてください。

へ進みます。


ナビゲーションのプリファレンスとして、新規ナビゲーション・メニュー・エントリの作成を選択します。

へ進みます。


データ・ソース表/ビューの名前としてEXC_DOCUMENTSを選択します。

へ進みます。


主キー型として主キー列の選択を選択し、主キー列ID (Number)を選択します。

作成をクリックします。


対話モード・レポートとフォームのページが作成されます。

最初に対話モード・レポートのページから修正します。

対話モード・レポートのリージョンを選択し、ソースのSQL問合せを以下の記述に置き換えます。サインインしたユーザーの現時点で有効な公開キーにて、アクセスが許可されているファイルを一覧します。



レポートのAttributesを開き、リンク列の設定をリンク列の除外に変更します。編集フォームへのリンクは、ソースのSELECT文の列IDに埋め込んでいます。自分自身が登録したファイルのみ、列に編集アイコンが表示されます。


編集リンクとなる列IDを選択し、セキュリティ特殊文字をエスケープOFFにします。SELECT文が返すHTML文書がエスケープされず、そのままHTMLとして解釈されます。


続いてフォームのページを編集します。ページ・デザイナにてページ番号を開きます。

ファイルを選択するページ・アイテムP4_BODYを選択します。ファイルのアップロード時にファイル名など必要な情報がデータベースに書き込まれるように、設定MIMEタイプ列としてBODY_MIMETYPEファイル名列としてBODY_FILENAME文字セット列としてBODY_CHARSETBLOB最終更新列としてBODY_LASTUPDを指定します。

ダウンロード・リンクの表示OFFにします。これがONだと、暗号化せずにファイルがダウンロードできてしまうため、必ずOFFにします


ページが作成ではなく編集で開かれた場合、実際には保存されているファイル(表EXC_DOCUMENTSの行)は更新しません。代わりに外部キー(表EXC_LINKSの行)を新たに作成します。

そのため、ページ・アイテムP4_IDに値が設定されているときは、ページ・アイテムP4_BODYが表示されないようにします。サーバー側の条件タイプアイテムはNULLを選択し、アイテムP4_IDを指定します。


同様の対応として、ページ・アイテムP4_NAMEを選択し、読取り専用タイプとしてアイテムはNULLではないを選択し、アイテムとしてP4_IDを指定します。


ページ・アイテムP4_BODY_FILENAMEP4_BODY_MIMETYPEP4_BODY_CHARSETP4_BODY_LASTUPDP4_SUBMITTED_BYP4_SUBMITTEDを選択し、構成ビルド・オプションとして、コメント・アウトを選択します。これらの列はユーザーによって指定されることはないのでフォームでの処理対象から除きますが、削除してしまうとコンテキスト・メニューよりページ・アイテムの同期化を実行すると、再度ページ・アイテムとして追加されてしまいます。そのため、ページ・アイテムとしては削除せず、ビルド・オプションで処理対象から除いています。


ボタンDELETEは使用しない(Immutable Tableなので削除できない)ため、削除します。


ファイルのアップロードや修正時に表EXC_LINKSを操作します。その処理に使用するページ・アイテムP4_LINK_IDを作成します。

リージョンファイルの登録上でページ・アイテムの作成を実行します。識別名前P4_LINK_IDタイプ非表示にします。


ファイルにアクセス可能なユーザーを選択するページ・アイテムP4_ACLSを作成します。

再度、ページ・アイテムの作成を実行します。識別名前P4_ACLSタイプチェック・ボックス・グループにします。ラベル許可とします。

LOVタイプSQL問合せを選択し、SQL問合せとして以下を記述します。

select username d, username r from exc_credentials_vw


ファイルを指定してフォームが開かれたときに、ページ・アイテムP4_LINK_IDとP4_ACLSにデータベースに保存されている値を移入するプロセスを作成します。

レンダリング前ヘッダーの前にプロセスを作成します。初期化フォームファイル登録の下に配置します。識別名前リンクIDとACLの移入とします。タイプには、コードの実行を選択します。ソースPL/SQLコードには以下を記述します。

サーバー側の条件タイプとしてアイテムはNULLではないを選択し、アイテムP4_IDを指定します。ファイルの新規アップロードではなく、すでにアップロードされたファイルを対象とした操作のときのみ、リンクIDとACLを読み込みます。


左ペインにてプロセス・ビューを開き、プロセス・フォームファイル登録を選択します。CREATEボタンが押されたときのみアップロードされたファイルを保存するように(SAVEでは保存しない)、サーバー側の条件ボタン押下時CREATEを指定します。


フォームが送信されたときに、表EXC_LINKSへの外部キーの登録と表EXC_ACLSへのアクセス権の設定を行うプロセスを作成します。

プロセスの作成を実行します。作成したプロセスはプロセス・フォーム文書登録ダイアログを閉じるの間に配置します。名前外部キーとACLの登録とします。ソースPL/SQLコードに以下を記述します。

サーバー側の条件タイプとしてリクエストは値に含まれるを選択し、CREATE,SAVEを指定します。ファイルのアップロードだけでなく編集時でもこのプロセスが実行され、新たな外部キーとACLの割り当てが行われます。


以上でファイルのアップロード画面の作成は完了です。

変更を保存して、ページを実行してみます。


ファイルを選択し作成をクリックするとExternal Key(外部キー)が割り当たります。ファイルの編集画面を表示し変更の適用をクリックすると、割り当たっていたExternal Keyが新しい値に置き換えられます。誰にもアクセスを許可しないように変更すると、アップロードしたファイルにアクセスできなくなります。表EXC_DOCUMENTSに行は残っていますが、論理的には削除された状態です。


ファイルのダウンロード



ファイルのダウンロードを行うページを作成します。

ページ作成ウィザードを起動し、空白ページを選択します。


ページ番号名前downloadとします。このページは直リンクにも使用するため、URLに現れるページ名は英語にします。ページ・モード標準オプションの静的コンテンツ・リージョンリージョン1として復号のための情報を入力し、作成されるページに静的コンテンツのリージョンを1つ含めます。

へ進みます。


このページは必ず外部キーの指定とともに呼び出され、ナビゲーション・メニューから引数なしで開かれることはありません。そのため、ナビゲーションのプリファレンスには、このページとナビゲーション・メニュー・エントリを関連付けないを選択します。

へ進みます。


確認画面が表示されるので、終了をクリックします。


ページが作成されます。

最初に直リンクを許可するために、ページ・プロパティのセキュリティディープ・リンク有効にします。


続けて、ダウンロードの処理に使用されるページ・アイテムを作成します。

最初にページ・アイテムIDを作成します。通常、ページ・アイテムにはPn_といったページ番号を含んだプリフィックスをつけますが、このIDはURLの引数になるためプリフィックスを外しています(URLにp5_id=ではなくid=で表示したい)。特殊な用法で、通常は推奨されません。

リージョン復号のための情報ページ・アイテムの作成を実行します。識別名前IDタイプとして非表示を選択します。


ソースセッション・ステートの保持セッションごと(ディスク)セキュリティセッション・ステート保護制限なしになっていることを確認します。デフォルトの設定なので変更する必要はありません。


AESによる暗号化で使用した初期ベクタを表示するためのページ・アイテムP5_IVを作成します。

ページ・アイテムの作成を実行し、識別名前P5_IVタイプ表示のみとします。 ラベル初期ベクタとします。


サインインしたユーザーのRSA公開キーで暗号化した(ダウンロードしたファイルのAESによる暗号化で使用した)共通キー(セッション・キー)を表示するためのページ・アイテムP5_ENCKEYを作成します。

ページ・アイテムの作成を実行し、識別名前P5_ENCKEYタイプ表示のみとします。ラベル暗号化したセッション・キーとします。


ファイルのダウンロードを実行するボタンを作成します。

リージョン復号のための情報ボタンの作成を実行します。

識別ボタン名としてDOWNLOADラベルダウンロードとします。動作アクションには動的アクションで定義を選択します。


左ペインでプロセス・ビューを開き、ファイルを暗号化してダウンロードするプロセスを作成します。

プロセスはAjaxコールバックとして作成します。Ajaxコールバック上でコンテキスト・メニューを開き、プロセスの作成を実行します。


作成されたプロセスの識別名前DOWNLOADソースPL/SQLコードに以下を記述します。



ボタンDOWNLOADをクリックしたときに、AjaxコールバックDOWNLOADを呼び出す動的アクションを作成します。

左ペインにレンダリング・ビューを表示します。ボタンDOWNLOAD上でコンテキスト・メニューを開き、動的アクションの作成を実行します。


動的アクションの識別名前ダウンロードの実行とします。タイミングは(ボタン上で動的アクションの作成を行なったため)デフォルトでイベントクリック選択タイプボタンボタンDOWNLOADとなっています。


作成されているTRUEアクションを選択し、識別アクションJavaScriptコードの実行に変更します。

設定コードとして以下を記述します。AjaxコールバックのDOWNLOADを呼び出すために、CGI変数のrequestAPPLICATION_PROCESS=DOWNLOADを指定し、表示中のページにGETリクエストを発行しています。

// "/ords/r/apexdev/"の部分は環境によって変更に必要がある
let url = "/ords/r/apexdev/" +
"&APP_ALIAS./&APP_PAGE_ALIAS.?session=&APP_SESSION." +
"&request=APPLICATION_PROCESS=DOWNLOAD";
// console.log(url);
apex.navigation.redirect(url);


あまり良い方法が見つからず、かなり強引なやり方なのですが、暗号処理で使われた初期ベクタとRSAで暗号化されたセッション・キー(共通鍵)を表示させます。

TRUEアクションの作成を実行し、JavaScriptコードの実行の下に配置します。

識別アクションとして値の設定を選択します。設定タイプの設定PL/SQL Expressionを選択し、PL/SQL式としてV('P5_IV')を記述します。データベースに保存されているP5_IVの値が画面上に設定されることを期待しています。影響を受ける要素選択タイプアイテムアイテムP5_IVとします。

初期化時に実行OFF結果を待機ONにします。


同様にして暗号化されたセッション・キーをページ・アイテムP5_ENCKEYに表示させる動的アクションを作成します。

先ほど作成したTRUEアクションを重複させ(TRUEアクション上でコンテキスト・メニューを表示させ重複を実行する)、PL/SQL式V('P5_ENCKEY')影響を受ける要素アイテムP5_ENCKEYに変更します。


ここで作成したページ・アイテムP5_IVおよびP5_ENCKEYに値を設定するTRUEアクションは、先行して定義されているJavaScriptコードの実行の終了を待ちません。そのため、値の設定にてV('P5_IV')またはV('P5_ENCKEY')を実行した時点で、それらの値がセッション・ステートに保持されている保証はありません。

ファイルのダウンロードが終了するのを待って画面上の値を更新する方法が見つからなかったため、同じアクション値の設定を複数回実行することにしました。それぞれのページ・アイテムについて、値の設定を2回実行するように、TRUEアクション重複させます。

重複させた後に値の設定の順番がP5_IV、P5_ENCKEY、P5_IV、P5_ENCKEYになるように位置を変えます。


それでも初期ベクタおよび暗号化したセッション・キーが表示されない場合は、ページをリロードすると表示されます。

最後に、このダウンロードのページがアクセスされたログと、引数IDに与えられた外部キーが有効化どうか確認するプロセスを作成します。

レンダリング前ヘッダーの前でプロセスを作成します。

作成したプロセスの識別名前外部キーの確認とします。ソースPL/SQLコードに以下を記述します。




以上で暗号化を行なった上でファイルをダウンロードするページが作成できました。


ダウンロード・ページの呼び出し



ファイル一覧より、ダウンロードを行うページを呼び出せるようにします。

ページ・デザイナにてファイル一覧のページ(ページ番号3)を開きます。

対話モード・レポートの列EXTERNAL_KEYを選択し、識別タイプリンクに変更します。


リンクターゲットをクリックし、リンク・ビルダー・ターゲットを開きます。ターゲットタイプこのアプリケーションのページページにはを選択します。アイテムの設定名前ID、対応する#EXTERNAL_KEY#を指定します。


ターゲットとなるダウンロード・ページを呼び出す際にキャッシュのクリアを行いたいのですが、クリア/リセットキャッシュのクリアとして5を指定すると、ページ番号5をアクセスするURLに引数clear=5が含まれてしまいます。

直リンクとして共有するURLはできるだけ単純にしたいため、キャッシュのクリアはここでは指定せずに、レポートのページがロードされたときに実行することにします。

レンダリング前ヘッダーの前でプロセスを作成します。作成したプロセスの識別名前ダウンロード・ページのクリアとし、ソースPL/SQLコードとして以下を記述します。

begin
apex_util.clear_page_cache(p_page_id => 5);
end;


ページの変更を保存します。以上でアプリケーションが完成しました。

アプリケーションを実行して動作の確認を行います。GIF動画では以下の操作を行なっています。
  1. ユーザーTEST1にてサインインし、ファイルtest1demo.txtをアップロードする。
  2. アップロードの際にユーザーTEST1とAPEXDEVにアクセス許可を与える。
  3. ユーザーAPEXDEVでサインインし、ファイルの一覧を表示する。
  4. ファイルの一覧よりtest1demo.txtを見つけ、外部キーのリンクをクリックする。
  5. test1demo.txtのダウンロードを実行する。
  6. test1demo.txtが暗号化されて、test1demo.txt.encとしてダウンロードされる。
  7. 画面上の初期ベクタ、暗号化されたセッション・キーを使って復号する。このときdecrypt.shというシェル・スクリプトを使用している。
  8. 復号されたファイルtest1demo.txtの内容を確認する。

ファイルの復号に使用しているdecrypt.shは以下です。


AppleのmacOSのzshで動作を確認しています。readコマンドの動きが異なるため、sh、bashではうまく動きませんでした。また、opensslは3.0.1を使用しています。Oracle Databaseに実装されたRSA暗号で暗号化したデータをopensslで復号するには、以下の指定が必須です。

-pkeyopt rsa_oaep_md:sha256

今回作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/fileexchangewithencrypt.sql

記事ではImmutable Tableを使っていますが、通常の表でも動作します。そのため、表とビューのDDLはエクスポートに含めていません。

以上で本記事は終了です。

Oracle APEXのアプリケーション作成の参考になれば幸いです。