0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WordPressプラグインで預かったAPIキーを、どう保存するか。saltから鍵を導いてAES-256-GCMで暗号化する

0
Posted at

先に答えを書きます。利用者から預かったAPIキーを update_option に平文で入れるのは、やめたほうがいいです。かといって鍵をコードに直書きしても意味がない。自分は、WordPressが既に持っている salt から鍵を導出して、AES-256-GCM で暗号化して保存しています。

AIチャットボットのプラグインを作っていて、利用者のAPIキーを預かる必要が出たとき、実際にこの形に落ち着きました。コードと、正直な限界を書きます。

なぜ平文がまずいか

update_option('myplugin_api_key', $key) と書くと、wp_options に平文で載ります。これが困るのは、DBの中身だけが漏れる経路がいくつもあるからです。

他のプラグインのSQLインジェクション。DBのバックアップファイルの置き場所ミス。共有サーバーでのDB露出。エクスポートしたSQLの取り扱い。どれも「サーバー全体は無事だが、DBの中身は見られた」という状況で、そのとき平文のAPIキーは、そのまま使われます。利用者の財布に直結する鍵なので、ここは一手間かける価値があります。

鍵をどこから持ってくるか

暗号化するなら鍵が要りますが、その鍵をどこに置くかで詰みます。コードに直書きしたら、プラグインを読めば誰でも復号できる。DBに置いたら、DBが漏れた時点で一緒に漏れる。

WordPressには、もともと wp-config.php に salt があります(AUTH_KEYSECURE_AUTH_SALT など)。これはDBではなくファイルにあり、サイトごとに違う。ここから鍵を導出すれば、「DBだけ漏れた」ケースでは復号されません。

/**
 * wp-config の salt から、このプラグイン専用の鍵を導出する。
 * salt そのものを鍵に使わず、HKDF で用途ごとに分ける。
 */
function myplugin_get_key(): string {
    $ikm = wp_salt('secure_auth'); // wp-config 側にある値
    // 用途(info)を分けておくと、他用途の鍵と混ざらない
    return hash_hkdf('sha256', $ikm, 32, 'myplugin/api-key/v1');
}

hash_hkdf は PHP 7.1.2 以降で使えます。salt を直接鍵にせず、用途を表す文字列(info)を混ぜて導出しておくと、あとで用途が増えたときに鍵が分けられます。

なお、プラグインから wp-config.php を書き換えるのは、WordPress.org の審査で止まります。既にある salt を読むだけなら、その心配はありません。

暗号化して保存する

AES-256-GCM を使います。認証付き(改ざんを検知できる)なので、単なる CBC より安全側です。

function myplugin_encrypt(string $plain): string {
    $key = myplugin_get_key();
    $iv  = random_bytes(12); // GCM は12バイト推奨
    $tag = '';

    $cipher = openssl_encrypt(
        $plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag
    );
    if ($cipher === false) {
        return '';
    }
    // iv + tag + 暗号文 をまとめて1本の文字列にする
    return base64_encode($iv . $tag . $cipher);
}

function myplugin_save_api_key(string $key): void {
    // autoload は no。毎リクエスト読む必要はない
    update_option('myplugin_api_key_enc', myplugin_encrypt($key), false);
}

IV は毎回ランダムに作り、暗号文と一緒に保存します(IVは秘密ではありません)。tag は改ざん検知用で、これが無いと復号できません。3つをまとめて1つの option に入れておくと、扱いが楽です。

復号する

保存の逆をやるだけです。

function myplugin_decrypt(string $stored): ?string {
    $raw = base64_decode($stored, true);
    if ($raw === false || strlen($raw) < 28) { // 12 + 16 + 本文
        return null;
    }
    $key    = myplugin_get_key();
    $iv     = substr($raw, 0, 12);
    $tag    = substr($raw, 12, 16);
    $cipher = substr($raw, 28);

    $plain = openssl_decrypt(
        $cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag
    );
    return $plain === false ? null : $plain;
}

復号に失敗したら null を返して、呼び出し側で「キーを再設定してください」と案内します。ここで例外を握り潰して空文字を返すと、原因が見えないまま「なぜか動かない」になります。

画面に出さない

保存できても、設定画面に平文で出したら台無しです。

// ❌ 復号して value に入れて表示
<input type="text" value="<?php echo esc_attr( myplugin_get_api_key() ); ?>">

// ✅ 保存済みかどうかだけ見せて、再入力させる
<input type="password" value="" placeholder="<?php echo $has_key ? '設定済み(変更する場合のみ入力)' : ''; ?>">

保存済みなら「設定済み」と出すだけ。値そのものは返しません。REST で設定を返す実装なら、そのレスポンスにキーが混ざっていないかも見ておきます。ログに出していないかも、一度 grep しておくと安心です。

正直なところ、これは万能ではありません

限界を書いておきます。この方法が守れるのは「DBだけが漏れた」ケースです。

サーバーのファイルまで読まれたら、wp-config.php の salt も読めるので、復号されます。つまり、フルにサーバーを取られた場合には効きません。それでも意味があるのは、実際の事故が「DBだけ」の形で起きることが多いからです。SQLインジェクション、バックアップの置き忘れ、DBの共有ミス。そこを塞ぐための一手間です。

もう一つ。salt を wp-config.php で変更すると、既存の暗号文は復号できなくなります。salt のローテーションは、それ自体は良い運用ですが、その時にキーの再設定が必要になることは、READMEなどに書いておくのが親切です。

早見でまとめ

  • APIキーを update_option に平文で入れない。DBだけ漏れる経路は実際にある
  • 鍵はコードにもDBにも置かない。wp-config の salt から hash_hkdf で導出する
  • aes-256-gcm + 毎回ランダムな12バイトIV。iv + tag + 暗号文 をまとめて保存
  • option は autoload = false。画面には値を返さず「設定済み」だけ出す
  • 守れるのは「DBだけ漏れた」ケース。サーバーごと取られたら効かない。salt変更で再設定が要る

利用者の鍵を預かるのは、預かった側の責任がそれなりに重い仕事です。完璧にはできませんが、いちばん起きやすい事故の形だけでも塞いでおくと、夜よく眠れます。役に立ったらストックして、キーを預かる実装のとき見返してください。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?