※ 本記事は2017年11月1日に公開されたものです。

アプリケーションで暗号化してからデータベースに格納する方法として、Oracle DatabaseではDBMS_CRYPTOというPL/SQLパッケージを用意しています。

なお、このDBMS_CRYPTOパッケージへのアクセス権限が制限されています。EXECUTE ANY PROCEDUREシステム権限やこの権限を含むDBAロールを付与されているユーザー、例えばSYSTEMユーザーであってもデフォルトの状態ではDBMS_CRYPTOパッケージを利用できません。パッケージ実行のためのオブジェクト権限を明示的に付与する必要があります。

DBAロールを持っているユーザーでも、デフォルトの状態ではDBMS_CRYPTOパッケージにはアクセスできないことが確認できます。

SQL> select role from session_roles where role='DBA';

ROLE
--------------------------------------------------------------------------------
DBA

SQL> desc dbms_crypto;
ERROR:
ORA-04043: オブジェクト"SYS"."DBMS_CRYPTO"は存在しません。

SYSユーザーでデータベースに接続し、DBMS_CRYPTOパッケージに対するオブジェクト権限の付与状況を調べても、誰にも付与されていないのが確認できます。

SQL> select grantee, privilege from dba_tab_privs where table_name='DBMS_CRYPTO';

レコードが選択されませんでした。

オブジェクト権限を明示的に付与することでユーザーはDBMS_CRYPTOパッケージを利用できるようになります。

SQL> grant execute on dbms_crypto to puku;

権限付与が成功しました。

SQL> select grantee, privilege from dba_tab_privs where table_name='DBMS_CRYPTO';

GRANTEE
--------------------------------------------------------------------------------
PRIVILEGE
----------------------------------------
PUKU
EXECUTE

次にDBMS_CRYPTOパッケージを利用して、暗号化/復号用のファンクションを作成します。

create or replace package myCrypto as
  function encrypt(var in varchar2) return raw;
  function decrypt(var in raw) return varchar2;
end;
/

create or replace package body myCrypto as
  myKey VARCHAR2(32) := '12345678901234567890123456789012';
  myType NUMBER := DBMS_CRYPTO.ENCRYPT_AES256 + DBMS_CRYPTO.CHAIN_CBC
                + DBMS_CRYPTO.PAD_PKCS5;

  rawKey RAW(32) := UTL_I18N.STRING_TO_RAW(myKey, 'AL32UTF8');

  function encrypt (var in varchar2) return raw is begin
    return dbms_crypto.encrypt(src => UTL_I18N.STRING_TO_RAW (var, 'AL32UTF8'),
                               typ => myType,
                               key => rawKey);
  end;

  function decrypt (var in raw) return varchar2 is begin
    return UTL_I18N.RAW_TO_CHAR(dbms_crypto.decrypt(src => var,
                                                    typ => myType,
                                                    key => rawKey),
                                'AL32UTF8');
  end;
end;
/

暗号化、復号は以下のように実施します。

SQL>select myCrypto.encrypt('サンプルデータ') from dual;

MYCRYPTO.ENCRYPT('サンプルデータ')
--------------------------------------------------------------------------------
5EFC472AA972468134144B2F9D679335F8AFDF7053E87DAE172CA56EBCABDAF8

SQL> select myCrypto.decrypt('5EFC472AA972468134144B2F9D679335F8AFDF7053E87DAE172CA56EBCABDAF8') from dual;

MYCRYPTO.DECRYPT('5EFC472AA972468134144B2F9D679335F8AFDF7053E87DAE172CA56EBCABDA
--------------------------------------------------------------------------------
サンプルデータ

暗号化したデータはRAW型ですので、格納する表の定義は文字列(VARCHAR2)型ではなく、RAW型で定義しなおす必要があります。

また、暗号化されたデータはブロック暗号アルゴリズムで暗号化されているため16バイト単位で切り上げられ、データ長が長くなることが確認できます。

SQL> select lengthb('サンプルデータ') from dual;

LENGTHB('サンプルデータ')
-------------------------
                       21

SQL> select utl_raw.length(myCrypto.encrypt('サンプルデータ')) from dual;

UTL_RAW.LENGTH(MYCRYPTO.ENCRYPT('サンプルデータ'))
--------------------------------------------------
                                                32

例えば暗号化前の表定義が、以下のようにVARCHAR2(30)だとします。

create table test_noenc (
  c1 varchar2(30));

このデータベースのキャラクタセットはAL32UTF8であるため、この列には10文字のデータを格納できます。日本語などマルチバイト文字の場合、10文字は暗号化ファンクションの中ではAL32UTF8キャラクタセットに変換して扱われますので、1文字が3バイトとなり、30バイトとなり、16バイト単位で切り上げられますので、以下のようにRAW(32)と定義する必要があります。ここではデータベースのキャラクタセットがAL32UTF8と1文字3バイトの文字コードのためサイズの差はあまり出ませんでしたが、SJISやEUCなど1文字2バイトの文字コードを利用している場合には、データサイズに差が出がちですので注意が必要です。

create table test_enc (
  c1 raw(32));

暗号化していない表に対しては通常のINSERT文を発行できますが、暗号化した表にINSERT文を発行する際には暗号化ファンクションをかませる必要があります。

insert into test_noenc values ('abc');
insert into test_noenc values ('あ');
insert into test_noenc values ('あいうえおかきくけこ');

insert into test_enc values (myCrypto.encrypt('abc'));
insert into test_enc values (myCrypto.encrypt('あ'));
insert into test_enc values (myCrypto.encrypt('あいうえおかきくけこ'));

それぞれの表を検索してみます。暗号化された表ではデータ長が長くなってしまっているのが確認できます。

SQL> select * from test_noenc;

C1
------------------------------
abc
あ
あいうえおかきくけこ

SQL> select * from test_enc;

C1
----------------------------------------------------------------
AB6B52A0228A71BCFC44232CA3E47640
71B09B2A35A1CEEC85706FF87138056C
147BE2B36BD8D95B060A17DA9DC781AE8DE3E9F6ECAECBE486CEEEBFA104759F

暗号化されていないデータを暗号化された表から取り出す場合には、復号ファンクションをかませます。

SQL> select myCrypto.decrypt(c1) from test_enc;

MYCRYPTO.DECRYPT(C1)
--------------------------------------------------------------------------------
abc
あ
あいうえおかきくけこ

DBMS_CRYPTOパッケージの使い方は以上です。

アプリケーションで暗号化してからデータベースに格納する方法としては、DBMS_CRYPTOパッケージを利用するほかにプログラム側で暗号化ロジックを組んだり、暗号化パッケージ製品を利用したりする方法が考えられます。ただし、どの方法を利用するにしても共通して以下の注意点があります。

  1. 暗号化によりデータ型やデータサイズが変わる
    暗号化したデータはバイナリ型になりますので表定義を変更する必要があります。バイナリ型を文字列として扱おうとすると、データ長がさらに長くなったり、変換のための追加の処理をしなければならなかったり、データが壊れてしまう可能性もあります。文字列型に暗号化する製品もあるかもしれませんが、その場合は利用している暗号アルゴリズムが電子政府推奨暗号リスト(CRYPTREC)やISO/IEC 18033で定義されている標準的で安全性が確認されているものから外れてしまう可能性があります。

  2. アプリケーションの書き換えが必要
    暗号化されたデータを扱うプログラムロジックのすべてに暗号化/復号のロジックを埋め込む必要があります。もし埋め込みを忘れてしまうとデータが破壊されてしまう可能性もあります。

  3. 索引の利用に制限(チューニングが困難)
    暗号化した列に索引を作成することは可能ですが、この索引は完全一致検索でしか利用できません。部分一致検索で索引を利用しようとすると結果0件となりますし、範囲検索に利用されてしまうと暗号化した後のデータで範囲検索されますので、全く関係ない結果が戻ってしまいます。また、今回利用したファンクションでは、同じ値を暗号化すると毎回暗号化された結果は同じになりますが、より安全に運用するために毎回出力される結果が変わるような(SALTを付けた)ファンクションを作成した場合、完全一致検索でも索引は利用できなくなります。
    参考までにSALTを付けた暗号化ファンクションは以下のように定義できます。

    create or replace package myCrypto2 as
      function encrypt(var in varchar2) return raw;
      function decrypt(var in raw) return varchar2;
    end;
    /
    
    create or replace package body myCrypto2 as
      myKey VARCHAR2(32) := '12345678901234567890123456789012';
      myType NUMBER := DBMS_CRYPTO.ENCRYPT_AES256 + DBMS_CRYPTO.CHAIN_CBC
                    + DBMS_CRYPTO.PAD_PKCS5;
      rawKey RAW(32) := UTL_I18N.STRING_TO_RAW(myKey, 'AL32UTF8');
      retval VARCHAR2(64);
    
      function encrypt (var in varchar2) return raw is begin
        return dbms_crypto.encrypt(src => UTL_I18N.STRING_TO_RAW (dbms_random.string('P',8) || var, 'AL32UTF8'),
                                   typ => myType,
                                   key => rawKey);
      end;
    
      function decrypt (var in raw) return varchar2 is begin
        retval := UTL_I18N.RAW_TO_CHAR(dbms_crypto.decrypt(src => var,
                                                           typ => myType,
                                                           key => rawKey),
                                       'AL32UTF8');
        return substr(retval,9,length(retval));
      end;
    end;
    /
    

    以下の例では3回暗号化していますが、それぞれ異なる暗号化データとなり、異なる暗号化データを復号すると同じ元の平文に戻ります。

    SQL> select myCrypto2.encrypt('あいうえお') from dual;
    
    MYCRYPTO2.ENCRYPT('あいうえお')
    --------------------------------------------------------------------------------
    141E841C942C339E9D088E9B173B165BA33C45401E66EE17DF2959E06C7E135E
    
    SQL> /
    
    MYCRYPTO2.ENCRYPT('あいうえお')
    --------------------------------------------------------------------------------
    6E4B6F039D9406AC57AF266B515AEA50421C8EAB4BF5D09F2AC774E502D1FC7D
    
    SQL> /
    
    MYCRYPTO2.ENCRYPT('あいうえお')
    --------------------------------------------------------------------------------
    500C87B6B47244C7D47DAD2C88243E2C2F2F9049B70192129F0D8C0E3163018F
    
    SQL> select myCrypto2.decrypt('141E841C942C339E9D088E9B173B165BA33C45401E66EE17DF2959E06C7E135E') from dual;
    
    MYCRYPTO2.DECRYPT('141E841C942C339E9D088E9B173B165BA33C45401E66EE17DF2959E06C7E1
    --------------------------------------------------------------------------------
    あいうえお
    
    SQL> select myCrypto2.decrypt('6E4B6F039D9406AC57AF266B515AEA50421C8EAB4BF5D09F2AC774E502D1FC7D') from dual;
    
    MYCRYPTO2.DECRYPT('6E4B6F039D9406AC57AF266B515AEA50421C8EAB4BF5D09F2AC774E502D1F
    --------------------------------------------------------------------------------
    あいうえお
    
    SQL> select myCrypto2.decrypt('500C87B6B47244C7D47DAD2C88243E2C2F2F9049B70192129F0D8C0E3163018F') from dual;
    
    MYCRYPTO2.DECRYPT('500C87B6B47244C7D47DAD2C88243E2C2F2F9049B70192129F0D8C0E31630
    --------------------------------------------------------------------------------
    あいうえお
    
  4. データベースアクセスによる情報漏洩は防げても、不正書換、削除には対応できない
    暗号化後にデータベースに格納しておけば、データベース管理者などがSELECT文でデータを見ようとしても、たしかに暗号化後のデータしか見ることができません。しかし、見えないだけであって、データにアクセスできないわけではありません。書き換えることはできるので、データ破壊を防ぐことはできません。また、データが破壊されているかどうかの確認は復号できるかどうか確認しないとわからないため、値の確認やデータパッチなどの管理操作のコストも増大します。
    (2017/11/7追記) 個人情報保護委員会の出している「個人情報の保護に関する法律についてのガイドライン」でも『「個人に関する情報」とは、… (中略) … 暗号化等によって秘匿化されているかどうかを問わない』と暗号化してあっても個人情報は個人情報でありアクセス制御などの安全管理措置を講じなければならないと明記されています。暗号化だけでは不正書き換え、削除には対応できないために、暗号化されていてもアクセス制御が必要ということもこの記述の理由のひとつだといえます。

  5. 鍵管理・配布が複雑
    暗号化パッケージを利用する場合には、暗号鍵の管理を自分でおこなわなくてはなりません。今回作成したファンクションでは暗号鍵をパッケージ内に記載しています。そのため、USER_SOURCEやDBA_SOURCEなどを以下のように検索すると、暗号鍵が見えてしまいます。

    select text from user_source where name='MYCRYPTO' and type='PACKAGE BODY' order by line;
    

    DBMS_DDLパッケージのWRAPファンクションやWRAPEDプロシージャを利用することで、パッケージのソースを分かりにくく(不明瞭化)することができますので、これらの機能と併せて利用する必要があります。なお、不明瞭化したソースは元に戻すことができませんので、別でパッケージのソースを保存しておく必要があります。

    また、暗号鍵の変更をおこなう仕組みがないため、暗号鍵の変更をおこなう方法を別途考えておく必要があります。

    外部の暗号化製品を利用する場合、暗号鍵管理や変更などの機能があるものがあるかもしれませんが、これらの製品を利用する場合、各アプリケーションに暗号鍵を含む製品を配布したり、暗号鍵やロジック自体は1か所のサーバーで管理している場合でも、暗号化・復号ロジックにアクセスするための認証情報を各アプリケーションに配布しなければならないため、管理コストはアプリケーション数分だけ増大してしまいます。

アプリケーションでの暗号化には様々な注意点があります。利用する際にはこれらの注意点を考慮して設計・設定してください。

「もくじ」にもどる