2022年12月9日金曜日

簡単なファイル管理アプリケーションの作成(7) - シークレットを使う

 暗号化キーの保存先をデータベースからOCIのボールトに変更してみました。暗号化キーは、ボールトのシークレットとして保存されます。

シークレットとしての操作は、以下の記事で作成したパッケージSFM_SECRET_UTILを使用します。

OCI PL/SQL SDKを使ってOCI Vaultのシークレットを操作する
http://apexugj.blogspot.com/2022/12/oci-secret-plsql-sdk.html

APEXアプリケーションとしての見た目や使い方に変更はありません。


表SFM_SECRETSの変更



暗号化キーを保存していた表SFM_SECRETSを作り直します。元の表はドロップし、以下の定義で表SFM_SECRETSを作成します。暗号化キーではなくシークレットのOCIDを保存します。
create table sfm_secrets (
    id                             number not null,
    version                        number not null,
    iv                             raw(16) not null,
    secret_ocid                  varchar2(400) not null,
    constraint sfm_secrets_pk primary key(id, version)
)
;


パッケージSFM_FILE_UTILの変更



変更したパッケージSFM_FILE_UTILのコードは以下になります。ファンクションの引数が増えているため、APEXアプリケーションの変更も必要です。

/**
SFM_FILE_UTIL - オブジェクト・ストレージをバックエンド・ストアとして使用する。
暗号化キーをOCIのシークレットとして保存するように改変。
パッケージ仕様
*/
create or replace package sfm_file_util as
/*
* 使用する暗号方式の定義
*/
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;
/**
IDで特定したSFM_CONETNTSの内容をオブジェクト・ストレージにアップロードする。
*/
procedure upload_file(
p_id in number
,p_region in varchar2
,p_namespace in varchar2
,p_bucket in varchar2
,p_credential in varchar2
-- OCI Secret
,p_compartment_ocid in varchar2
,p_master_key_ocid in varchar2
,p_vault_ocid in varchar2
);
/**
IDで特定したSFM_CONTENTSの内容としてオブジェクト・ストレージに保存されている
ファイルを取り出す。
*/
procedure download_file(
p_id in number
,p_region in varchar2
,p_namespace in varchar2
,p_bucket in varchar2
,p_credential in varchar2
-- OCI Secret
,p_vault_ocid in varchar2
);
/**
IDに対応したオブジェクト・ストレージに保存されているファイルを削除する。
*/
procedure delete_file(
p_id in number
,p_region in varchar2
,p_namespace in varchar2
,p_bucket in varchar2
,p_credential in varchar2
-- OCI Secret
,p_time_of_deletion in timestamp with time zone
);
end;
/
/**
パッケージ本体
*/
create or replace package body sfm_file_util as
procedure upload_file(
p_id in number
,p_region in varchar2
,p_namespace in varchar2
,p_bucket in varchar2
,p_credential in varchar2
-- OCI Secret
,p_compartment_ocid in varchar2
,p_master_key_ocid in varchar2
,p_vault_ocid in varchar2
)
as
l_response dbms_cloud_oci_obs_object_storage_put_object_response_t;
l_next_version number;
l_object_name varchar2(32767);
e_file_upload_exception exception;
/* 暗号化のための追加 */
l_enc_key raw(32); -- 文書をAES256で暗号化する鍵
l_iv raw(16); -- 初期ベクタ
l_secret_ocid sfm_secrets.secret_ocid%type;
l_secret_name varchar2(400);
l_content blob;
begin
for c in (select content_filename, version, content, content_mimetype from sfm_contents where id = p_id)
loop
/*
* シークレットIDを取得する。
*/
begin
select iv, secret_ocid into l_iv, l_secret_ocid
from sfm_secrets where id = p_id;
exception
when no_data_found then
l_iv := dbms_crypto.randombytes(16);
l_secret_ocid := null;
end;
l_enc_key := dbms_crypto.randombytes(C_NUM_KEY_BYTES);
/*
* Secret OCIDが取得できなければ、ボールトにシークレットを新規に作成する。
*/
l_secret_name := 'sfm-' || p_id; -- シークレット名の書式はsfm-IDで固定。
if l_secret_ocid is null then
/* 新規作成 */
l_secret_ocid := sfm_secret_util.create_secret(
p_key => l_enc_key
,p_secret_name => l_secret_name
,p_compartment_ocid => p_compartment_ocid
,p_master_key_ocid => p_master_key_ocid
,p_vault_ocid => p_vault_ocid
,p_region => p_region
,p_credential => p_credential
,p_current_version_number => l_next_version
);
insert into sfm_secrets(id, version, iv, secret_ocid) values(p_id, l_next_version, l_iv, l_secret_ocid);
else
/* アップデート */
l_next_version := sfm_secret_util.update_secret(
p_key => l_enc_key
,p_secret_id => l_secret_ocid
,p_region => p_region
,p_credential => p_credential
);
update sfm_secrets set version = l_next_version where id = p_id;
end if;
/*
* 暗号化を行うコード。
*/
dbms_lob.createTemporary(l_content, TRUE, dbms_lob.call);
dbms_crypto.encrypt(
dst => l_content
,src => c.content
,typ => C_SHARED_ENC_TYPE
,key => l_enc_key
,iv => l_iv
);
/*
* 暗号化に関するコードは終了。
*/
-- アップロードするファイル名を決める。
l_object_name := p_id || '/' || l_next_version || '/' || utl_url.escape(c.content_filename, false, 'AL32UTF8');
-- アップロードを実行する。
l_response := dbms_cloud_oci_obs_object_storage.put_object
(
namespace_name => p_namespace
,bucket_name => p_bucket
,object_name => l_object_name
,content_type => c.content_mimetype
,put_object_body => l_content
,region => p_region
,credential_name => p_credential
);
if l_response.status_code <> 200 then
raise e_file_upload_exception;
end if;
-- 表SFM_CONTENTSをアップデート。DBに保存されているBLOBのデータは消去する。
update sfm_contents set version = l_next_version, content = null where id = p_id;
end loop;
end upload_file;
procedure download_file(
p_id in number
,p_region in varchar2
,p_namespace in varchar2
,p_bucket in varchar2
,p_credential in varchar2
-- OCI Secret
,p_vault_ocid in varchar2
)
as
l_response dbms_cloud_oci_obs_object_storage_get_object_response_t;
l_filename sfm_contents.content_filename%type;
l_mime_type sfm_contents.content_mimetype%type;
l_version sfm_contents.version%type;
l_object_name varchar2(32767);
l_download apex_data_export.t_export;
e_file_download_exception exception;
/* 復号のための追加 */
l_enc_key raw(32); -- 復号に使う鍵
l_iv raw(16); -- 初期ベクタ
l_secret_ocid sfm_secrets.secret_ocid%type;
l_secret_name varchar2(400);
l_content blob;
begin
-- オブジェクト・ストレージ上の名前をl_object_nameとして取り出す。
select version, content_filename, content_mimetype
into l_version, l_filename, l_mime_type
from sfm_contents
where id = p_id;
l_object_name := p_id || '/' || l_version || '/' || utl_url.escape(l_filename, false, 'AL32UTF8');
-- オブジェクト・ストレージから対象ファイルを取り出す。
l_response := dbms_cloud_oci_obs_object_storage.get_object(
namespace_name => p_namespace
,bucket_name => p_bucket
,object_name => l_object_name
,region => p_region
,credential_name => p_credential
);
if l_response.status_code <> 200 then
raise e_file_download_exception;
end if;
/*
* シークレットから暗号化キーを取り出す。
*/
select iv into l_iv from sfm_secrets where id = p_id;
l_secret_name := 'sfm-' || p_id;
l_enc_key := sfm_secret_util.get_key_from_secret(
p_secret_name => l_secret_name
,p_vault_ocid => p_vault_ocid
,p_region => p_region
,p_credential => p_credential
,p_secret_id => l_secret_ocid
,p_version_number => l_version
);
dbms_lob.createTemporary(l_content, TRUE, dbms_lob.call);
dbms_crypto.decrypt(
dst => l_content
,src => l_response.response_body
,typ => C_SHARED_ENC_TYPE
,key => l_enc_key
,iv => l_iv
);
/*
* 復号に関するコード追加はここまで。
*/
-- 取り出したファイルを、ブラウザにダウンロードする。
l_download.file_name := l_filename;
l_download.mime_type := l_mime_type;
l_download.as_clob := false;
l_download.content_blob := l_content;
apex_data_export.download( p_export => l_download );
apex_application.stop_apex_engine;
end download_file;
procedure delete_file(
p_id in number
,p_region in varchar2
,p_namespace in varchar2
,p_bucket in varchar2
,p_credential in varchar2
-- OCI Secret
,p_time_of_deletion in timestamp with time zone
)
as
l_list_response dbms_cloud_oci_obs_object_storage_list_objects_response_t;
l_response dbms_cloud_oci_obs_object_storage_delete_object_response_t;
l_object_name varchar2(32767);
e_file_delete_exception exception;
l_secret_ocid sfm_secrets.secret_ocid%type;
begin
/* ID以下のファイルの一覧を取得する。 */
l_object_name := p_id || '/';
l_list_response := dbms_cloud_oci_obs_object_storage.list_objects(
prefix => l_object_name
,namespace_name => p_namespace
,bucket_name => p_bucket
,region => p_region
,credential_name => p_credential
);
if l_list_response.status_code <> 200 then
raise e_file_delete_exception;
end if;
/* フォルダに含まれるファイルをひとつひとつ削除する。 */
for i in 1..l_list_response.response_body.objects.count
loop
l_object_name := l_list_response.response_body.objects(i).name;
apex_debug.info(l_object_name);
l_response := dbms_cloud_oci_obs_object_storage.delete_object(
namespace_name => p_namespace
,bucket_name => p_bucket
,object_name => l_object_name
,region => p_region
,credential_name => p_credential
);
if l_response.status_code not in (200,204) then
apex_debug.info('status_code = %s', l_response.status_code);
raise e_file_delete_exception;
end if;
end loop;
/* 保存している暗号キーを削除する。 */
select secret_ocid into l_secret_ocid from sfm_secrets where id = p_id;
sfm_secret_util.delete_secret(
p_secret_id => l_secret_ocid
,p_time_of_deletion => p_time_of_deletion
,p_region => p_region
,p_credential => p_credential
);
delete from sfm_secrets where id = p_id;
end delete_file;
end sfm_file_util;
/

APEXアプリケーションの更新



シークレットを操作するPL/SQL SDKはコンパートメントマスター暗号化キーおよびボールトOCIDを引数に取ります。そのため、アプリケーション定義置換文字列としてG_COMPARTMENT_OCIDG_MASTER_KEY_OCIDG_VAULT_OCIDを追加します。

それぞれの値の取得方法は、パッケージSFM_SECRET_UTILに関する記事に記載があります。



パッケージSFM_FILE_UTILを呼び出しているプロセスに対して、パラメータの同期を実行します。

プロセス上でコンテキスト・メニューを表示させると、パラメータの同期が含まれています。


プロセスファイルのアップロードでは、パラメータとしてp_compartment_ocidp_master_key_ocidp_vault_ocidが追加されています。これらはそれぞれ置換文字列として追加したG_COMPARTMENT_OCIDG_MASTER_KEY_OCIDG_VAULT_OCIDを割り当てます。


プロセスファイルの削除では、パラメータとしてp_time_of_deletionが追加されています。こちらは、PL/SQL式として以下を記述します。

systimestamp + (interval '2' day)

削除したファイルに紐づくシークレットを、2日後に削除するようスケジュールします。


プロセスダウンロードでは、パラメータとしてp_vault_ocidが追加されています。これには置換文字列のG_VAULT_OCIDを割り当てます。


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

OCIのシークレットはこのような用途を想定しているのかどうかはわかりませんが、PL/SQL SDKのサンプルにはなっているでしょう。

また、APEX 22.2から使えるようになったプロセス・タイプAPIの呼び出しですが、本当に生産性が上がります。PL/SQLのコードをパッケージにまとめられるため、単体テストなども実施が容易です。

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

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