
最近めっぽうセキュリティに詳しくなってきた、自称独自セキュリティコーディネーターです(ウソ)。
データの受け渡しをする時に、データが改竄(かいざん)されないための仕組みを構築するのは、
システムエンジニアとして当然の仕事です。
どんな簡易なシステムであっても、webで公開型でかつ、個人情報を取得するシステムであれば、
セキュリティが考慮されていないと、法律違反に値すると考えてもいいでしょう。
例えるなら、素っ裸で渋谷の交差点を走っている様なものです。(意味不明かも)
バックエンドエンジニアであれば、CGI系のサーバー再度スクリプトで、ブラックボックス化して、ある程度簡易な方法でセキュア処理を実装することができますが、
これがネイティブJavascript(いわゆるフロントサイド)においてのセキュリティは、そうもいきません。
javascriptでは、データもそのデータをどのようにコンバートするかも、プログラム自体が丸見えになるため、
何をやってもセキュアというわけには行かないため、エンジニアはとても対応に困る場合があるんですね。(あまりここにこだわったエンジニアがいないのも事実かも)
でも、それを払拭したいと考えるのが、この自称セキュリティコーディネーターの自負です。
というわけで、今回は、Javascriptで2段階keyを用いた暗号化と、暗号データをPHPで復元するコードを掲載しておきたいと思います。
ちなみに、その逆のPHP->Javascriptも可能な状態です。
Javascriptコード
crypto.js
/**
* 暗号化処理(PHP互換性あり)
*
* [options]
* - text : 暗号化したいテキストデータ(encryptの場合にセット)
* - enc_str : 暗号化されたデータ(decryptの場合にセット)
* - session_id : セッションID
* - secret_key : シークレットキー
*
* [demo]
* - encrypt
* new Crypto({
* type : "encrypt",
* data : "Hello!",
* session_id : session_id,
* secret_key : secret_key,
* }).promise.then((res)=>{ console.log(res)} )
*
* - decrypt
* new Crypto({
* type : "decrypt",
* data : "t0ldhaODrbJp+5NgwuBOJA==",
* session_id : session_id,
* secret_key : secret_key,
* }).promise.then((res)=>{ console.log(res)} )
*/
export class Crypto{
constructor(options){
options = options || {}
this.promise = new Promise((resolve, reject)=>{
this.resolve = resolve
this.reject = reject
if(!options.session_id || !options.secret_key){
this.finish()
}
this.init(options)
})
}
async init(options){
switch(options.type){
case "encrypt" :
const res1 = await Crypto.encrypt(options.data, options.session_id, options.secret_key)
this.finish(res1)
break
case "decrypt":
const res2 = await Crypto.decrypt(options.data, options.session_id, options.secret_key)
this.finish(res2)
break
}
}
// 暗号化
static async encrypt(text, sessionId, secretKey) {
const encoder = new TextEncoder();
const rawKey = await crypto.subtle.digest("SHA-256", encoder.encode(sessionId + secretKey));
const iv = encoder.encode("1234567890123456"); // 16byte固定
const key = await crypto.subtle.importKey(
"raw", rawKey, { name: "AES-CBC" }, false, ["encrypt"]
);
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-CBC", iv: iv },
key,
encoder.encode(text)
);
// Base64変換(表示用)
const base64 = btoa(String.fromCharCode(...new Uint8Array(encrypted)));
return base64;
}
// 複合化
static async decrypt(cipherTextBase64, sessionId, secretKey) {
const encoder = new TextEncoder();
const rawKey = await crypto.subtle.digest("SHA-256", encoder.encode(sessionId + secretKey));
const iv = encoder.encode("1234567890123456"); // 暗号化時と同じIVが必要
const key = await crypto.subtle.importKey(
"raw", rawKey, { name: "AES-CBC" }, false, ["decrypt"]
);
// Base64をUint8Arrayに変換
const binary = atob(cipherTextBase64);
const cipherBytes = new Uint8Array([...binary].map(c => c.charCodeAt(0)));
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-CBC", iv: iv },
key,
cipherBytes
);
return new TextDecoder().decode(decrypted);
}
finish(data){
this.resolve(data)
}
}
PHPコード
crypto.php
<?php
/**
* 文字列暗号化処理(javascript互換性あり)
*/
class Crypto{
function __construct($plainText, $sessionId, $secretKey){
}
// 暗号化
public static function encrypt($plainText, $sessionId, $secretKey) {
$key = hash('sha256', $sessionId . $secretKey, true); // 32byteキー
$iv = "1234567890123456"; // 暗号化と同じIV(16byte)
$encrypted = openssl_encrypt($plainText, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($encrypted);
}
// 複合化
public static function decrypt($base64CipherText, $sessionId, $secretKey) {
$key = hash('sha256', $sessionId . $secretKey, true); // 32byteキー
$iv = "1234567890123456"; // 暗号化と同じIV(16byte)
$ciphertext = base64_decode($base64CipherText);
$decrypted = openssl_decrypt($ciphertext, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return $decrypted;
}
}
How to use
基本的な使い方としては、今回は、
暗号化したい文字列と、
公開鍵、
暗号鍵の3つのデータを用いて、全てが揃わないと復元化できない文字列を作ってそれを復元するという仕様になります。
Javascriptでの処理コードサンプル
<script type="module">
import { Crypto } from './crypto.js'
const session_id = "cf12720837904e66fe27cc4f429a3dab"
const secret_key = "secretXYZ"
const data = "あいうえおかきくけこさしすせそたちつてと"
// エンコード
new Crypto({
type:"encrypt",
data : data,
session_id: session_id,
secret_key: secret_key
}).promise.then((res)=>{
console.log(res)
// デコード
new Crypto({
type:"decrypt",
data : res,
session_id: session_id,
secret_key: secret_key
}).promise.then((res)=>{
console.log(res)
})
})
</script>
Javascript実行結果
暗号化:
RlaA7E9IexHA5b1BB/OlvsE8EDbL3QbHFLzQJhk2GGskenkMjMyL1FTUB2VzMcatZ7oUH8oowE2jQj0IbdFfmg==
複合化:
あいうえおかきくけこさしすせそたちつてと
PHPでの処理コードサンプル
<pre>
<?php
require_once(__DIR__."/../php/crypto.php");
$text = "あいうえおかきくけこさしすせそたちつてと";
$session_id = "sess1234";
$secret_key = "secretXYZ";
$enc = Crypto::encrypt($text, $session_id, $secret_key);
echo $enc.PHP_EOL;
$dec = Crypto::decrypt($enc, $session_id, $secret_key);
echo $dec.PHP_EOL;
PHP実行結果
暗号化:
RlaA7E9IexHA5b1BB/OlvsE8EDbL3QbHFLzQJhk2GGskenkMjMyL1FTUB2VzMcatZ7oUH8oowE2jQj0IbdFfmg==
複合化:
あいうえおかきくけこさしすせそたちつてと
解説
JavascriptもPHPも同じ実行結果になっているので、相互互換があるという状態です。
ちなみに、Javascriptは、ライブラリなどは使わず、このコードだけでエンコードができてしまいますが、非同期で結果を受け取る必要があるので、
実行結果は、promise.then()で受け取るか、async~awaitで受けとる必要があるので、要注意です。
あと肝心な、
秘密鍵をjavascriptで取り扱うにはどうすりゃいいんやねん問題に関しては、PHPで発行したsessionn_idまたは、任意発行した識別子をjavascriptでうけとって、
判定することで、ブラウザ単位での鍵切り替えができるようになるのと、ちゃんとセッション管理ができていれば、少し変則的暗号鍵というやり方で、セキュアを保てます。
参考 :
[PHP] sessionについての深掘り
あとがき
今回のコードは、あまり使う機会が無い可能性も高いのですが、テキストデータをやりとりすることが多いWebシステムで手軽にコピペで活用できる様にしようという、自己備忘録でもありました。
こんなバニラコードよりも、composerやnpmでサクッとpullした方が楽と考えている人には、まるで役に立たないブログでしたが、
このブログを面白がる人の傾向として、変態エンジニア(褒め言葉)が多いという事実もあるようですよ。
信じるか信じないかはあなた次第・・・
0 件のコメント:
コメントを投稿