どうも、hide92795です。
Bukkit RemoteControllerのBukkitDev上に「iPhone(WinPhone)のも作ってくれ」という要望が結構あったので、本来は来年にMacを買ってからネイティブアプリとして作る予定でしたが、なんか審査に通らない気がしてきたので、Webアプリとして作ることにしました。
JavaScriptなにこれ?おいしいの?状態からのスタートでしたが、なんとか1週間でメインの通信可能な状態まで持って行くことが出来ました。
というわけで、今回はそこでやっていたJavaとJavaScript間での暗号化について書いていこうと思います。
自分の実装ではサーバー側がJava、クライアント側がJavaScript、
Java側でRSA鍵を生成、RSA公開鍵をJavascriptへ送信、Javascriptで生成したAES共通鍵をRSAで暗号化して送信、Java側でそれを復号という手順になっています。
また、通信の方法はWebSocket(Java側はTooTallNate/Java-WebSocket、JavaScript側はデフォルトのもの)を使っています。
まず、Java側でのRSA鍵の生成から
1 2 3 4 5 6 7 8 9 10 |
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA"); keygen.initialize(1024); KeyPair keyPair = keygen.generateKeyPair(); privateKey = (RSAPrivateKey) keyPair.getPrivate(); publicKey = (RSAPublicKey) keyPair.getPublic(); String modules = publicKey.getModulus().toString(); String publicExponent = publicKey.getPublicExponent().toString(); String data = modules + ":" + publicExponent; |
WebSocketで通信する際には、普通の文字(String)で送信できるので、ModulesとPublicExponentを16進数の文字列表現に変換して、コロンでつなげています。
次に、JavaScript側でのAES鍵作成とそれの暗号化へ
JavaScript側での暗号化では、RSA暗号化に以下のものを使用します。
- RSA and ECC in JavaScript : http://www-cs-students.stanford.edu/~tjw/jsbn/
- jsbn.js, jsbn2.js, prng4.js, rng.js, base64.jsを使用
- A Client-Side JavaScript RSA Encrypter : http://www.peterrowntree.com/?d0=papers/clientRSA
- rsa.jsを使用
- crypto-js : http://code.google.com/p/crypto-js/
- aes.js, pbkdf2.jsを使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var datas = event.data.split(":"); //Key gen var salt = CryptoJS.lib.WordArray.random(128/8); var key = CryptoJS.PBKDF2(random_str(16), salt, { keySize: 128/32 }); var key_b64 = CryptoJS.enc.Base64.stringify(key); var modules = new BigInteger(datas[0]); var publicExponent = new BigInteger(datas[1]); var rsa = new RSAKey(); rsa.setPublic(modules.toString(16), publicExponent.toString(16)); var common_key_encrypted = rsa.encrypt(key_b64); var common_key_base64_encoded = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(common_key_encrypted)); |
AESキーはPBKDF2でランダムに生成(random_strは指定された文字数の英数字文字列を返す関数)
受信した文字列をコロンで区切って、それからRSAKeyを作成。
rsa.encryptで返されるのは、16進数文字列なので、crypto-jsでBase64に変換
Base64に変換したものをJava側に送り返します。
ちなみに、この部分をJavaで行うと、こんな感じになります。
- Base64Coderは http://www.source-code.biz/base64coder/java/
- Base64はTooTallNate Java-WebSocketの中にあった「org.java_websocket.util.Base64」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
String[] rsa_key_sa = data.split(":"); String modules_s = rsa_key_sa[0]; String publicExponent_s = rsa_key_sa[1]; BigInteger modules = new BigInteger(modules_s); BigInteger publicExponent = new BigInteger(publicExponent_s); RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modules, publicExponent); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); // Gen key char[] password = UUID.randomUUID().toString().toCharArray(); SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); byte[] salt = new byte[16]; random.nextBytes(salt); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); KeySpec spec = new PBEKeySpec(password, salt, 10, 128); SecretKey tmp = factory.generateSecret(spec); this.key = tmp.getEncoded(); byte[] common_key_base64 = Base64.encodeBytesToBytes(key); byte[] common_key_encrypted = cipher.doFinal(common_key_base64); char[] common_key_base64_encoded = Base64Coder.encode(common_key_encrypted); String data = String.valueOf(common_key_base64_encoded); |
違いは、AES鍵を生成する際にUUIDを使用しているくらいですかね
次はJava側での復号化
1 2 3 4 5 |
byte[] receive_key_decoded = Base64Coder.decode(data); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] key_b64 = cipher.doFinal(receive_key_decoded); byte[] key = Base64.decode(key_b64); |
ここで出てきたbyte[] keyがAES暗号鍵です。
ここからはAESを使った暗号化です。
まず、Java側での暗号化はこんな感じです
1 2 3 4 5 6 7 8 9 10 11 |
SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] iv = cipher.getIV(); byte[] encrypted = cipher.doFinal(text.getBytes(Charset.forName("UTF-8"))); String iv_b64 = String.valueOf(Base64Coder.encode(iv)); String encrypted_b64 = String.valueOf(Base64Coder.encode(encrypted)); result = iv_b64 + ":" + encrypted_b64; |
モードがAES/CBC/PKCS5なので、IVを送る必要があります。
このIVを送るために、IVと暗号化したデータをそれぞれBase64で符号化、連結しているので多少ややこしくなってしまっています。
(Java同士の実装だと、byte[]を連結させればよかったのですが、JSと組み合わせた時にIVの長さが15byteになったりと不都合があったのでこうなりました)
次に、Java側での復号化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
String[] datas = text.split(":"); String iv_b64 = datas[0]; String encrypted_b64 = datas[1]; byte[] iv = Base64Coder.decode(iv_b64); byte[] encrypted = Base64Coder.decode(encrypted_b64); Key keySpec = new SecretKeySpec(key.get(), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec ivspec = new IvParameterSpec(iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivspec); byte[] resultBytes = cipher.doFinal(encrypted); result = new String(resultBytes, Charset.forName("UTF-8")); |
まぁ、暗号化とは逆な感じですね。
最初にコロンで区切ってあるIVと暗号文を分け、それぞれを復号、交換したAES鍵を使用して復号化といった感じです。
次はこれを作り始めて一番時間がかかったJavaScript側の実装です。
まずは暗号化のほうから
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var encrypt_data = CryptoJS.enc.Utf8.parse(data); var iv = CryptoJS.lib.WordArray.random(16); var encrypted = CryptoJS.AES.encrypt(encrypt_data, this.key, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv : iv }); var iv_b64 = CryptoJS.enc.Base64.stringify(encrypted.iv); var encrypted_b64 = CryptoJS.enc.Base64.stringify(encrypted.ciphertext); var send_data = iv_b64 + ":" + encrypted_b64; |
やっている事は
- 暗号化するデータをWordArrayに変換
- IVをランダムに生成
- データを暗号化
- IVと暗号化したデータをそれぞれBase64化
- それらをコロンでつなげる
と言った感じです。
最後は復号化の部分です
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var datas = data.split(":"); var iv_b64 = datas[0]; var encrypted_b64 = datas[1]; var iv = CryptoJS.enc.Base64.parse(iv_b64); var encrypted_data = CryptoJS.enc.Base64.parse(encrypted_b64); var encrypted = { iv: iv, key: this.key, ciphertext: encrypted_data }; var decrypted = CryptoJS.AES.decrypt(encrypted, this.key, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: iv }); var decrypted_str = CryptoJS.enc.Utf8.stringify(decrypted); |
一番の謎は、CryptoJS.AES.decryptに渡すencryptedの中身にivとkeyを渡さないとエラーが出るということです。
JavaScriptは引数にどのクラス?の変数を渡していいのかがわかりにくいのでもう大変でしたw
あとthisの仕組みが未だによくわからん(´・ω・`)
まぁ、とりあえずは動いているので、細かな修正は色々組み上げて大分わかってきてからにしましょうかね
ではノシ