Oracle Databaseに組み込みのRSAとAESの暗号処理を使って、ファイルを交換するアプリケーションを試作してみました。
以下の機能を実装します。
- ユーザーはそれぞれ自身のRSA公開キーを登録する。管理者が確認した上で登録した方が、セキュリティ面では有利だと思います。
- ファイルをアップロードする。アップロードする際にダウンロードを許可するユーザーを選択する。
- 許可されているファイルをダウンロードする。ダウンロードの際にセッション・キーを生成し、ファイルはセッション・キーを使ってAESで暗号化する。セッション・キーは登録済みのRSA公開キーで暗号化し、AESの初期ベクタと共に画面に表示する。
- 手元にダウンロードしたファイルは、初期ベクタ、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の定義は以下になります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# prefix: exc | |
# semantics: default | |
# ondelete: restrict | |
credentials -- 認証されたユーザーごとのRSA公開鍵 | |
username vc80 /nn | |
public_key clob | |
submitted_by vc80 /default sys_context('apex$session','app_user') /nn | |
submitted date /default sysdate /nn | |
documents -- アップロードされたファイル | |
name vc200 /nn | |
body file | |
submitted_by vc80 /default sys_context('apex$session','app_user') /nn | |
submitted date /default sysdate /nn | |
links -- ファイルのダウンロードに指定する外部キー | |
document_id /reference documents | |
external_key vc64 /nn | |
submitted_by vc80 /default sys_context('apex$session','app_user') /nn | |
submitted date /default sysdate /nn | |
acls -- ダウンロードを許可する公開キー | |
link_id /references links /nn | |
credential_id /references credentials /nn | |
submitted_by vc80 /default sys_context('apex$session','app_user') /nn | |
submitted date /default sysdate /nn | |
downloads -- 成功したファイルのダウンロードの履歴 | |
session num /nn | |
username vc80 /nn | |
credential_id /references credentials /nn | |
link_id /references links /nn | |
document_id /references documents /nn | |
submitted_by vc80 /default sys_context('apex$session','app_user') /nn | |
submitted date /default sysdate /nn | |
access_log -- すべてのダウンロード要求の履歴 | |
session num | |
username vc80 | |
external_key vc64 | |
referer vc4000 | |
x-forwarded-for vc4000 | |
submitted_by vc80 /default sys_context('apex$session','app_user') /nn | |
submitted date /default sysdate /nn |
クイック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 daysと16 daysの部分)はこの通りでなくても問題ないですが、0 daysの方の日付を0より大きくすると表のドロップができなくなります。そのため、アプリケーションが完成するまでは0 daysの指定が便利です。
- 列SUBMITTED_BYのデフォルト値の指定より'(アポストロフィ)を取り除く。
以上の変更を6つのcreate table文すべてで実施します。
変更したDDLは以下になります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- create tables | |
create immutable table exc_credentials ( | |
id number generated by default on null as identity | |
constraint exc_credentials_id_pk primary key, | |
username varchar2(80) not null, | |
public_key clob, | |
submitted_by varchar2(80) default SYS_CONTEXT('APEX$SESSION','APP_USER') not null, | |
submitted date default SYSDATE not null | |
) | |
no drop until 0 days idle | |
no delete until 16 days after insert | |
; | |
-- comments | |
comment on table exc_credentials is '認証されたユーザーごとのRSA公開鍵'; | |
create immutable table exc_documents ( | |
id number generated by default on null as identity | |
constraint exc_documents_id_pk primary key, | |
name varchar2(200) not null, | |
body blob, | |
body_filename varchar2(512), | |
body_mimetype varchar2(512), | |
body_charset varchar2(512), | |
body_lastupd date, | |
submitted_by varchar2(80) default SYS_CONTEXT('APEX$SESSION','APP_USER') not null, | |
submitted date default SYSDATE not null | |
) | |
no drop until 0 days idle | |
no delete until 16 days after insert | |
; | |
-- comments | |
comment on table exc_documents is 'アップロードされたファイル'; | |
create immutable table exc_links ( | |
id number generated by default on null as identity | |
constraint exc_links_id_pk primary key, | |
document_id number | |
constraint exc_links_document_id_fk | |
references exc_documents, | |
external_key varchar2(64) not null, | |
submitted_by varchar2(80) default SYS_CONTEXT('APEX$SESSION','APP_USER') not null, | |
submitted date default SYSDATE not null | |
) | |
no drop until 0 days idle | |
no delete until 16 days after insert | |
; | |
-- table index | |
create index exc_links_i1 on exc_links (document_id); | |
-- comments | |
comment on table exc_links is 'ファイルのダウンロードに指定する外部キー'; | |
create immutable table exc_acls ( | |
id number generated by default on null as identity | |
constraint exc_acls_id_pk primary key, | |
link_id number | |
constraint exc_acls_link_id_fk | |
references exc_links not null, | |
credential_id number | |
constraint exc_acls_credential_id_fk | |
references exc_credentials not null, | |
submitted_by varchar2(80) default SYS_CONTEXT('APEX$SESSION','APP_USER') not null, | |
submitted date default SYSDATE not null | |
) | |
no drop until 0 days idle | |
no delete until 16 days after insert | |
; | |
-- table index | |
create index exc_acls_i1 on exc_acls (credential_id); | |
create index exc_acls_i2 on exc_acls (link_id); | |
-- comments | |
comment on table exc_acls is 'ダウンロードを許可する公開キー'; | |
create immutable table exc_downloads ( | |
id number generated by default on null as identity | |
constraint exc_downloads_id_pk primary key, | |
credential_id number | |
constraint exc_downloads_credential_id_fk | |
references exc_credentials not null, | |
link_id number | |
constraint exc_downloads_link_id_fk | |
references exc_links not null, | |
document_id number | |
constraint exc_downloads_document_id_fk | |
references exc_documents not null, | |
the_session number not null, | |
username varchar2(80) not null, | |
submitted_by varchar2(80) default SYS_CONTEXT('APEX$SESSION','APP_USER') not null, | |
submitted date default SYSDATE not null | |
) | |
no drop until 0 days idle | |
no delete until 16 days after insert | |
; | |
-- table index | |
create index exc_downloads_i1 on exc_downloads (credential_id); | |
create index exc_downloads_i2 on exc_downloads (document_id); | |
create index exc_downloads_i3 on exc_downloads (link_id); | |
-- comments | |
comment on table exc_downloads is '成功したファイルのダウンロードの履歴'; | |
create immutable table exc_access_log ( | |
id number generated by default on null as identity | |
constraint exc_access_log_id_pk primary key, | |
the_session number, | |
username varchar2(80), | |
external_key varchar2(64), | |
referer varchar2(4000), | |
x_forwarded_for varchar2(4000), | |
submitted_by varchar2(80) default SYS_CONTEXT('APEX$SESSION','APP_USER') not null, | |
submitted date default SYSDATE not null | |
) | |
no drop until 0 days idle | |
no delete until 16 days after insert | |
; | |
-- comments | |
comment on table exc_access_log is 'すべてのダウンロード要求の履歴'; | |
-- load data |
スクリプトの変更後、実行をクリックし、表示された確認画面で即時実行をクリックします。すべてのDDLが成功していることを確認します。
Immutable Tableとして作成しているため、表EXC_CREDENTIALSおよびEXC_LINKSには無効となった過去のデータが含まれます。それぞれ有効な列のみが選択されるビューEXC_CREDENTIALS_VWとEXC_LINKS_VWを定義します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace view exc_credentials_vw(id, username, public_key) | |
as | |
select id, username, public_key | |
from exc_credentials | |
where 1=1 | |
and id in (select max(id) from exc_credentials group by username) | |
and public_key is not null | |
/ | |
create or replace view exc_links_vw(id, document_id, external_key) | |
as | |
select l.id, l.document_id, l.external_key | |
from exc_links l join | |
( | |
select max(id) id, document_id | |
from exc_links group by document_id | |
) r | |
on l.id = r.id and l.document_id = r.document_id | |
/ |
実行はSQLワークショップのSQLコマンドより、それぞれのビューごとに実行するとよいでしょう。SQLスクリプトとして実行することもできます。
以上で、アプリケーションが必要としているデータベース・オブジェクトの準備は完了です。
初期アプリケーションの作成
アプリケーション・ビルダーより新規アプリケーションの作成を実行し、アプリケーション作成ウィザードを起動します。
アプリケーションの名前をファイル交換とし、アプリケーションの作成を実行します。他に特別な設定は行いません。
アプリケーションが作成されたら、最初にアプリケーションの別名を変更します。アプリケーション・プロパティの編集をクリックします。
アプリケーションの別名をfileexchangeとし、変更の適用をクリックします。アプリケーションの別名は(簡易URLがONの場合)URLに現れるため、英数字に限定するのが望ましいです。
ビルド・オプションとしてコメント・アウトを作成します。APEX 21.2からはデフォルトで作成されているため、APEX 21.2を使用している場合はこの作業は不要です。
共有コンポーネントのビルド・オプションを開きます。
作成をクリックします。
ビルド・オプションにコメント・アウトと入力します。ステータスは除外、エクスポートのデフォルトも除外とします。コンポーネントのビルド・オプションにコメント・アウトを選択すると、そのコンポーネントは存在しないのと同じ扱いになります。
作成されたビルド・オプションが一覧されます。
以上でアプリケーションの準備は完了です。
RSA公開キーの登録
アプリケーションにユーザーがRSA公開キーを登録するページを作成します。
ページの作成を実行し、ページ作成ウィザードを起動します。
フォームを選択します。
公開キーは登録するだけなので、単体のフォームを選択します。
ページ番号は2とします。ページ名は公開キーの登録、ページ・モードは標準を選択します。送信時にここにブランチは、ページの遷移が行われないように2、取り消してページに移動も2とします。
次へ進みます。
ナビゲーションのプリファレンスとして新規ナビゲーション・メニュー・エントリの作成を選択します。新規ナビゲーション・メニュー・エントリはデフォルトで公開キーの登録になります。
次へ進みます。
データ・ソースのソース・タイプとしてSQL問合せを選択し、SQL SELECT文を入力に以下を記述します。
select
id
,username
,public_key
from exc_credentials
where username = :APP_USER
主キー列にID (Number)を選択します。作成をクリックします。
ページが作成されます。
このページから公開キーを登録するときに、常に列USERNAMEにサインインしたユーザー(APP_USER)が設定されるように、ページ・アイテムP2_USERNAMEを調整します。
ページ・アイテムP2_USERNAMEを選択し、タイプを非表示に変更します。
作成された計算の実行オプションのポイントとして送信後を選択し、計算のタイプにアイテム、アイテム名にAPP_USERを指定します。この計算により、P2_USERNAMEの値は常にAPP_USERの値(サインインしたユーザー名)となります。
リージョン・ボタンのCANCEL、DELETE、SAVEは使用しないので、これらのボタンを選択したのちコンテキスト・メニューを開いて削除を実行します。
公開キーを入力するページ・アイテム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、フォーム・ページ名はファイル登録とします。ページ・アイテムの名称に影響があるので、ページ番号は3、4としてください。
次へ進みます。
ナビゲーションのプリファレンスとして、新規ナビゲーション・メニュー・エントリの作成を選択します。
次へ進みます。
データ・ソースの表/ビューの名前としてEXC_DOCUMENTSを選択します。
次へ進みます。
主キー型として主キー列の選択を選択し、主キー列にID (Number)を選択します。
作成をクリックします。
対話モード・レポートとフォームのページが作成されます。
最初に対話モード・レポートのページから修正します。
対話モード・レポートのリージョンを選択し、ソースのSQL問合せを以下の記述に置き換えます。サインインしたユーザーの現時点で有効な公開キーにて、アクセスが許可されているファイルを一覧します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
select | |
case | |
when d.submitted_by = :APP_USER then | |
'<a href="' || | |
apex_page.get_url( | |
p_page => 4 | |
, p_clear_cache => 4 | |
, p_items => 'P4_ID' | |
, p_values => d.id | |
) || | |
'"><span aria-label="編集"><span class="fa fa-edit" aria-hidden="true" title="編集"></span></span></a>' | |
else | |
null | |
end id | |
,d.name | |
,d.body_filename filename | |
,l.external_key | |
from exc_links_vw l | |
join exc_documents d on l.document_id = d.id | |
join exc_acls a on l.id = a.link_id | |
join exc_credentials_vw c on a.credential_id = c.id | |
where c.username = :APP_USER |
レポートのAttributesを開き、リンク列の設定をリンク列の除外に変更します。編集フォームへのリンクは、ソースのSELECT文の列IDに埋め込んでいます。自分自身が登録したファイルのみ、列に編集アイコンが表示されます。
編集リンクとなる列IDを選択し、セキュリティの特殊文字をエスケープをOFFにします。SELECT文が返すHTML文書がエスケープされず、そのままHTMLとして解釈されます。
続いてフォームのページを編集します。ページ・デザイナにてページ番号4を開きます。
ファイルを選択するページ・アイテムP4_BODYを選択します。ファイルのアップロード時にファイル名など必要な情報がデータベースに書き込まれるように、設定のMIMEタイプ列としてBODY_MIMETYPE、ファイル名列としてBODY_FILENAME、文字セット列としてBODY_CHARSET、BLOB最終更新列としてBODY_LASTUPDを指定します。
ダウンロード・リンクの表示はOFFにします。これがONだと、暗号化せずにファイルがダウンロードできてしまうため、必ずOFFにします。
ページが作成ではなく編集で開かれた場合、実際には保存されているファイル(表EXC_DOCUMENTSの行)は更新しません。代わりに外部キー(表EXC_LINKSの行)を新たに作成します。
そのため、ページ・アイテムP4_IDに値が設定されているときは、ページ・アイテムP4_BODYが表示されないようにします。サーバー側の条件のタイプにアイテムはNULLを選択し、アイテムにP4_IDを指定します。
同様の対応として、ページ・アイテムP4_NAMEを選択し、読取り専用のタイプとしてアイテムはNULLではないを選択し、アイテムとしてP4_IDを指定します。
ページ・アイテムP4_BODY_FILENAME、P4_BODY_MIMETYPE、P4_BODY_CHARSET、P4_BODY_LASTUPD、P4_SUBMITTED_BY、P4_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コードには以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_link_id exc_links.id%type; | |
l_acls varchar2(4000); | |
begin | |
-- 最新のリンクIDを取得する。 | |
select max(id) into l_link_id | |
from exc_links where document_id = :P4_ID | |
group by document_id; | |
:P4_LINK_ID := l_link_id; | |
-- リンクIDに紐づいたACLを取得する。 | |
select listagg(c.username,':') into l_acls | |
from exc_acls a join exc_credentials c | |
on a.credential_id = c.id | |
where a.link_id = l_link_id; | |
:P4_ACLS := l_acls; | |
end; |
サーバー側の条件のタイプとしてアイテムはNULLではないを選択し、アイテムにP4_IDを指定します。ファイルの新規アップロードではなく、すでにアップロードされたファイルを対象とした操作のときのみ、リンクIDとACLを読み込みます。
左ペインにてプロセス・ビューを開き、プロセス・フォームファイル登録を選択します。CREATEボタンが押されたときのみアップロードされたファイルを保存するように(SAVEでは保存しない)、サーバー側の条件のボタン押下時にCREATEを指定します。
フォームが送信されたときに、表EXC_LINKSへの外部キーの登録と表EXC_ACLSへのアクセス権の設定を行うプロセスを作成します。
プロセスの作成を実行します。作成したプロセスはプロセス・フォーム文書登録とダイアログを閉じるの間に配置します。名前は外部キーとACLの登録とします。ソースのPL/SQLコードに以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* アクセス・コントロールは表EXC_LINKSに行を追加し、その行に公開キーを紐づける。 | |
* ACLの作成も更新も、まったく同じ操作を行う。 | |
*/ | |
declare | |
l_external_key exc_links.external_key%type; | |
l_link_id exc_links.id%type; | |
l_document_id exc_documents.id%type; | |
begin | |
/* | |
* ファイルを共有するために、外部に公開するキー値を(暗号)乱数より生成する。 | |
* ファイルのダウンロード時は、そのキー値(表EXC_LINKSのEXTERNAL_KEY)を指定する。 | |
* 内部のID(表EXC_DOCUMENTSのID)は公開しない。 | |
*/ | |
l_document_id := :P4_ID; | |
l_external_key := rawtohex(dbms_crypto.randombytes(16)); | |
insert into exc_links(document_id, external_key) | |
values(l_document_id, l_external_key) returning id into l_link_id; | |
:P4_LINK_ID := l_link_id; -- ページ・アイテムに設定する。 | |
/* | |
* ファイルのダウンロードはexternal_keyを指定して行われる。 | |
* このexternal_keyにクリデンシャル(表EXC_CREDENTIALSの最新のID)を紐づけて | |
* アクセスを制限する。 | |
* ユーザーの公開キーがクリデンシャルになり、公開キーが置き換えられると | |
* 過去にダウンロードできた資料もダウンロードできなくなる。 | |
*/ | |
insert into exc_acls(link_id, credential_id) | |
select l_link_id, id from exc_credentials_vw | |
where username in | |
( | |
select column_value from table(apex_string.split(:P4_ACLS,':')) | |
); | |
end; |
サーバー側の条件のタイプとしてリクエストは値に含まれるを選択し、値にCREATE,SAVEを指定します。ファイルのアップロードだけでなく編集時でもこのプロセスが実行され、新たな外部キーとACLの割り当てが行われます。
以上でファイルのアップロード画面の作成は完了です。
変更を保存して、ページを実行してみます。
ファイルを選択し作成をクリックするとExternal Key(外部キー)が割り当たります。ファイルの編集画面を表示し変更の適用をクリックすると、割り当たっていたExternal Keyが新しい値に置き換えられます。誰にもアクセスを許可しないように変更すると、アップロードしたファイルにアクセスできなくなります。表EXC_DOCUMENTSに行は残っていますが、論理的には削除された状態です。
ファイルのダウンロード
ファイルのダウンロードを行うページを作成します。
ページ作成ウィザードを起動し、空白ページを選択します。
ページ番号は5、名前はdownloadとします。このページは直リンクにも使用するため、URLに現れるページ名は英語にします。ページ・モードは標準、オプションの静的コンテンツ・リージョンのリージョン1として復号のための情報を入力し、作成されるページに静的コンテンツのリージョンを1つ含めます。
次へ進みます。
このページは必ず外部キーの指定とともに呼び出され、ナビゲーション・メニューから引数なしで開かれることはありません。そのため、ナビゲーションのプリファレンスには、このページとナビゲーション・メニュー・エントリを関連付けないを選択します。
次へ進みます。
確認画面が表示されるので、終了をクリックします。
ページが作成されます。
最初に直リンクを許可するために、ページ・プロパティのセキュリティのディープ・リンクを有効にします。
続けて、ダウンロードの処理に使用されるページ・アイテムを作成します。
最初にページ・アイテムIDを作成します。通常、ページ・アイテムにはPn_といったページ番号を含んだプリフィックスをつけますが、このIDはURLの引数になるためプリフィックスを外しています(URLにp5_id=ではなくid=で表示したい)。特殊な用法で、通常は推奨されません。
リージョン復号のための情報でページ・アイテムの作成を実行します。識別の名前をID、タイプとして非表示を選択します。
ソースのセッション・ステートの保持がセッションごと(ディスク)、セキュリティのセッション・ステート保護が制限なしになっていることを確認します。デフォルトの設定なので変更する必要はありません。
ページ・アイテムの作成を実行し、識別の名前をP5_IV、タイプを表示のみとします。 ラベルは初期ベクタとします。
サインインしたユーザーのRSA公開キーで暗号化した(ダウンロードしたファイルのAESによる暗号化で使用した)共通キー(セッション・キー)を表示するためのページ・アイテムP5_ENCKEYを作成します。
ページ・アイテムの作成を実行し、識別の名前をP5_ENCKEY、タイプを表示のみとします。ラベルは暗号化したセッション・キーとします。
ファイルのダウンロードを実行するボタンを作成します。
リージョン復号のための情報でボタンの作成を実行します。
識別のボタン名としてDOWNLOAD、ラベルはダウンロードとします。動作のアクションには動的アクションで定義を選択します。
左ペインでプロセス・ビューを開き、ファイルを暗号化してダウンロードするプロセスを作成します。
プロセスはAjaxコールバックとして作成します。Ajaxコールバック上でコンテキスト・メニューを開き、プロセスの作成を実行します。
作成されたプロセスの識別の名前をDOWNLOAD、ソースのPL/SQLコードに以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 初期ベクタ、共有鍵を生成し、共有鍵はアクセスしたユーザーの公開キーで暗号化する。 | |
* ファイルはAES256で暗号化し、初期ベクタ、暗号化した共有鍵は画面に表示する。 | |
*/ | |
declare | |
C_NUM_KEY_BYTES constant number := 256/8; -- 鍵長は256bit | |
C_SHARED_ENC_TYPE constant PLS_INTEGER := -- AES256, CBC, PKCS5 | |
DBMS_CRYPTO.ENCRYPT_AES256 | |
+ DBMS_CRYPTO.CHAIN_CBC | |
+ DBMS_CRYPTO.PAD_PKCS5; | |
l_shared_key raw(32); -- 文書をAES256で暗号化する鍵 | |
l_iv raw(16); -- 初期ベクタ | |
l_public_key varchar2(4000); -- RSA公開鍵 | |
l_shared_key_enc raw(4000); -- RSAで暗号化した共通鍵 | |
l_body blob; -- AES256で暗号化する文書 | |
l_body_enc blob; -- 暗号化した文書 | |
l_document_id exc_documents.id%type; | |
l_filename varchar2(200); -- アップロード時の文書ファイル名 | |
l_external_key exc_links.external_key%type; | |
l_credential_id exc_credentials.id%type; | |
l_link_id exc_links.id%type; | |
-- ダウンロードのログを書き込む。 | |
procedure log_download( | |
p_session in number | |
, p_username in varchar2 | |
, p_credential_id number | |
, p_link_id in number | |
, p_document_id in number | |
) | |
is | |
pragma autonomous_transaction; | |
begin | |
insert into exc_downloads(the_session, username, credential_id, link_id, document_id) | |
values (p_session, p_username, p_credential_id, p_link_id, p_document_id); | |
commit; | |
end; | |
begin | |
-- 外部キーの保存。 | |
l_external_key := :ID; | |
-- 初期ベクタと鍵を生成する。 | |
l_iv := DBMS_CRYPTO.RANDOMBYTES(16); | |
l_shared_key := DBMS_CRYPTO.RANDOMBYTES(C_NUM_KEY_BYTES); | |
-- 外部キーより、ダウンロードするドキュメントのIDを取得する。 | |
select id, document_id into l_link_id, l_document_id | |
from exc_links_vw where external_key = l_external_key | |
and id in | |
( | |
select link_id from exc_acls | |
where credential_id in | |
( | |
select id from exc_credentials_vw | |
where username = :APP_USER | |
) | |
); | |
-- 指定された文書を取り出し、AES256で暗号化する。 | |
select body, body_filename into l_body, l_filename | |
from exc_documents where id = l_document_id; | |
dbms_lob.createTemporary(l_body_enc, TRUE, dbms_lob.session); | |
dbms_crypto.encrypt( | |
dst => l_body_enc | |
, src => l_body | |
, typ => C_SHARED_ENC_TYPE | |
, key => l_shared_key | |
, iv => l_iv | |
); | |
-- サインインしているユーザーのRSA公開鍵を取り出す。 | |
select id, public_key into l_credential_id, l_public_key from exc_credentials_vw | |
where username = :APP_USER; | |
-- PEM形式の公開鍵のヘッダーとフッターを取り除く。 | |
l_public_key := regexp_replace( | |
l_public_key, | |
'(-+((BEGIN|END) (PUBLIC|PRIVATE) KEY)-+\s?|\s)', | |
'' | |
); | |
-- AES256で使った共通鍵をRSAで暗号化する。 | |
l_shared_key_enc := dbms_crypto.pkencrypt( | |
src => l_shared_key | |
, pub_key => utl_i18n.string_to_raw(l_public_key, 'AL32UTF8') | |
, pubkey_alg => dbms_crypto.key_type_rsa | |
, enc_alg => dbms_crypto.pkencrypt_rsa_pkcs1_oaep | |
); | |
-- 安全のため、使用済みの共通鍵を参照不可にする。 | |
l_shared_key := null; | |
-- 初期ベクタと暗号化した共通鍵をページ・アイテムに設定する。 | |
-- 代入でコミットするはずだが、画面への反映のために明示してコミットする。 | |
-- :P5_IV := rawtohex(l_iv); | |
apex_util.set_session_state('P5_IV', rawtohex(l_iv),true); | |
-- :P5_ENCKEY := rawtohex(l_shared_key_enc); | |
apex_util.set_session_state('P5_ENCKEY', rawtohex(l_shared_key_enc),true); | |
-- ダウンロードの履歴を書き込む。自律トランザクションなので、失敗しても履歴は残る。 | |
log_download(:APP_SESSION, :APP_USER, l_credential_id, l_link_id, l_document_id); | |
-- 暗号化したファイルをダウンロードする。ファイル名の末尾に.encを付加する。 | |
sys.htp.init; | |
sys.htp.p('Content-Length: ' || dbms_lob.getlength(l_body_enc)); | |
sys.htp.p('Content-Type: application/octet-stream'); | |
sys.htp.p('Content-Disposition: attachment; filename=' || l_filename || '.enc'); | |
sys.owa_util.http_header_close; | |
sys.wpg_docload.download_file(l_body_enc); | |
dbms_lob.freeTemporary(l_body_enc); | |
end; |
ボタンDOWNLOADをクリックしたときに、AjaxコールバックDOWNLOADを呼び出す動的アクションを作成します。
左ペインにレンダリング・ビューを表示します。ボタンDOWNLOAD上でコンテキスト・メニューを開き、動的アクションの作成を実行します。
動的アクションの識別の名前はダウンロードの実行とします。タイミングは(ボタン上で動的アクションの作成を行なったため)デフォルトでイベントはクリック、選択タイプはボタン、ボタンはDOWNLOADとなっています。
作成されているTRUEアクションを選択し、識別のアクションをJavaScriptコードの実行に変更します。
設定のコードとして以下を記述します。AjaxコールバックのDOWNLOADを呼び出すために、CGI変数のrequestにAPPLICATION_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にします。
先ほど作成した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コードに以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 引数のIDが外部キーとして有効化どうか確認する。 | |
*/ | |
declare | |
l_external_key exc_links.external_key%type; | |
l_document_id exc_documents.id%type; | |
-- アクセスのログを書き込む。 | |
procedure log_access( | |
p_session in number | |
, p_username in varchar2 | |
, p_external_key in varchar2 | |
) | |
is | |
l_referer exc_access_log.referer%type; | |
l_xforwardedfor exc_access_log.x_forwarded_for%type; | |
pragma autonomous_transaction; | |
begin | |
l_referer := owa_util.get_cgi_env('Referer'); | |
l_xforwardedfor := owa_util.get_cgi_env('X-Forwarded-For'); | |
insert into exc_access_log(the_session, username, external_key, referer, x_forwarded_for) | |
values (p_session, p_username, p_external_key, l_referer, l_xforwardedfor); | |
commit; | |
end; | |
begin | |
-- ページ呼び出しのログ。 | |
log_access(:APP_SESSION, :APP_USER, :ID); | |
-- 外部キーが有効化かどうか確認する。 | |
l_external_key := :ID; | |
select document_id into l_document_id | |
from exc_links where external_key = l_external_key | |
and id in | |
( | |
select link_id from exc_acls | |
where credential_id in | |
( | |
select id from exc_credentials_vw | |
where username = :APP_USER | |
) | |
); | |
end; |
以上で暗号化を行なった上でファイルをダウンロードするページが作成できました。
ダウンロード・ページの呼び出し
ファイル一覧より、ダウンロードを行うページを呼び出せるようにします。
ページ・デザイナにてファイル一覧のページ(ページ番号3)を開きます。
対話モード・レポートの列EXTERNAL_KEYを選択し、識別のタイプをリンクに変更します。
ターゲットとなるダウンロード・ページを呼び出す際にキャッシュのクリアを行いたいのですが、クリア/リセットのキャッシュのクリアとして5を指定すると、ページ番号5をアクセスするURLに引数clear=5が含まれてしまいます。
直リンクとして共有するURLはできるだけ単純にしたいため、キャッシュのクリアはここでは指定せずに、レポートのページがロードされたときに実行することにします。
レンダリング前、ヘッダーの前でプロセスを作成します。作成したプロセスの識別の名前をダウンロード・ページのクリアとし、ソースのPL/SQLコードとして以下を記述します。
begin
apex_util.clear_page_cache(p_page_id => 5);
end;
ページの変更を保存します。以上でアプリケーションが完成しました。
アプリケーションを実行して動作の確認を行います。GIF動画では以下の操作を行なっています。
- ユーザーTEST1にてサインインし、ファイルtest1demo.txtをアップロードする。
- アップロードの際にユーザーTEST1とAPEXDEVにアクセス許可を与える。
- ユーザーAPEXDEVでサインインし、ファイルの一覧を表示する。
- ファイルの一覧よりtest1demo.txtを見つけ、外部キーのリンクをクリックする。
- test1demo.txtのダウンロードを実行する。
- test1demo.txtが暗号化されて、test1demo.txt.encとしてダウンロードされる。
- 画面上の初期ベクタ、暗号化されたセッション・キーを使って復号する。このときdecrypt.shというシェル・スクリプトを使用している。
- 復号されたファイルtest1demo.txtの内容を確認する。
ファイルの復号に使用しているdecrypt.shは以下です。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/zsh | |
iv=$1 | |
enckey=$2 | |
efile=$3 | |
# check existance of source/encrypt file. | |
if [ ! -e ${efile} ]; then | |
echo no such file ${efile}; exit 1 | |
fi | |
# define filename for destination/decyrpt file. | |
dfile=$4 | |
if [ -z "${dfile}" ]; then | |
# trim .enc from source filename for default dest filename. | |
dfile=${efile%.enc} | |
fi | |
# avoid overwite | |
if [ -e ${dfile} ]; then | |
echo ${dfile} exists; exit 1 | |
fi | |
# decrypt synmetric key, the key kept in skey | |
echo $enckey |\ | |
xxd -r -ps |\ | |
openssl pkeyutl -decrypt -inkey private.pem -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 |\ | |
xxd -ps -c 80 | read skey | |
# for debug. | |
# echo encrypt filename is $efile | |
# echo decrypt filename is $dfile | |
# echo IV is $iv | |
# echo KEY is $skey | |
# decrypt download file. | |
openssl aes-256-cbc -d -in ${efile} -out ${dfile} -K ${skey} -iv ${iv} | |
exit; |
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のアプリケーション作成の参考になれば幸いです。
完