2022年12月8日木曜日

簡単なファイル管理アプリケーションの作成(6) - ファイルの暗号化

 オブジェクト・ストレージにファイルを保存するにあたってセキュリティが気になったので、ファイルをオブジェクト・ストレージにアップロードするときにAES256で暗号化、ダウンロードした後ブラウザに戻す際に復号するように実装してみました。

(追記: 引数opc_sse_customer_keyを指定することによりオブジェクト・ストレージに保存するファイルを暗号化できるので、そちらを使う方が正解でした。)

暗号キーと初期ベクターはデータベースに保存します。

ファイルをデータベースに保存していれば、オラクル・データベースが提供する各種のアクセス制御の機能(Database Vault、Virtual Private Database、Real Application Securityなど)が使えますが、ファイルを丸ごとデータベースのストレージに保存するのは高コストです。コストを下げるためにオブジェクト・ストレージにオフロードすると、セキュリティが心配になります。暗号化してファイルをオブジェクト・ストレージにオフロードすれば、それだけが盗まれても読めませんし、また、改ざんもできません。

データベースに暗号キーが保存されているので、それはアクセス制御をかけて保護する必要があります。ただし、ファイル丸ごとよりは容量を消費しません。

以下より実施した作業を説明します。


DBMS_CRYPTOの実行権限の付与


データベースでの暗号化および復号にはパッケージDBMS_CRYPTOを使用します。このパッケージを使用するため、APEXのワークスペース・スキーマにDBMS_CRYPTOの実行権限を与えます。

grant execute on dbms_crypto to <APEXのワークスペース・スキーマ>;

Autonomous Databaseでは、データベース・アクション開発SQLから実行します。APEXのワークスペース名がAPEXDEVである場合は、以下のgrant文を実行します。

grant execute on dbms_crypto to wksp_apexdev;


暗号キーを保存する表の作成


暗号キーと初期ベクターを保存する表SFM_SECRETSを作成します。

create table sfm_secrets (
    id                             number not null,
    version                        number not null,
    iv                             raw(16) not null,
    key                            raw(32) not null,
    constraint sfm_secrets_pk primary key(id, version)
)
;

SQLワークショップSQLコマンドから実行します。


パッケージSFM_FILE_UTILの置き換え


パッケージSFM_FILE_UTILを暗号化処理を組み込んだものに置き換えます。以下のコードを実行します。アップロードを行うプロシージャupload_fileではDBMS_CRYPTO.ENCRYPTを呼び出してファイルの内容を暗号化しています。download_fileではDBMS_CRYPTO.DECRYPTを呼び出し、ファイルを復号しています。それぞれ追加した行数は数行です。

/**
SFM_FILE_UTIL - オブジェクト・ストレージをバックエンド・ストアとして使用する。
パッケージ仕様
*/
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
);
/**
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
);
/**
IDに対応したオブジェクト・ストレージに保存されているファイルを削除する。
*/
procedure delete_file(
p_id in number
,p_region in varchar2
,p_namespace in varchar2
,p_bucket in varchar2
,p_credential in varchar2
);
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
)
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_content blob;
begin
for c in (select content_filename, version, content, content_mimetype from sfm_contents where id = p_id)
loop
-- 次のバージョン番号を取得する
l_next_version := nvl(c.version, 0) + 1;
/*
* 暗号化を行うコード。
*/
l_content := c.content; -- 暗号化に関するコードを削除しても動作させる。
-- 暗号化の実行
l_iv := dbms_crypto.randombytes(16);
l_enc_key := dbms_crypto.randombytes(C_NUM_KEY_BYTES);
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
);
insert into sfm_secrets(id, version, iv, key) values(p_id, l_next_version, l_iv, l_enc_key);
/*
* 暗号化に関するコード追加はここまで。
*/
-- アップロードするファイル名を決める。
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
)
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_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;
/*
* 復号を行うコード。
*/
l_content := l_response.response_body; -- 復号に関するコードを削除しても動作させる。
-- 復号の実行
select iv, key into l_iv, l_enc_key
from sfm_secrets where id = p_id and version = 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
)
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;
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;
/* 保存している暗号キーを削除する。 */
delete from sfm_secrets where id = p_id;
end delete_file;
end sfm_file_util;
/

APEXアプリケーションへの変更は不要です。