先に答えを書きます。利用者から預かった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_KEY や SECURE_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まわりのことを書いています。