≒ UWP(C++/CX)→Java間のRSA+AES暗号化/復号化覚え書き

ここ何日か悪戦苦闘したので覚え書き。我ながら迂遠かつトリッキーな処理してるので、何をやってるのか忘れてしまう可能性が高いし。
なお、ここに記す処理の諸々は本番環境ではそのまま使う気は(当然)無いです。暗号については(も)ど素人なので、セキュリティ上の穴がある可能性が大であることは最初に述べておきます。

まずはRSA鍵生成。最初はJavaで生成するを見つけてその通りやろうとして、鍵データの保存/呼び出し方法で四苦八苦。
でもよく考えたらこの部分は普通にOpenSSL使えばいいんじゃないか?情報も多いし。参考
[code language=”bash”]
openssl genrsa -out private_key.pem
openssl rsa -pubout -in private_key.pem -out public_key.pem
openssl rsa -inform pem -outform der -pubin -in public_key.pem -out public_key.der
openssl pkcs8 -topk8 -in private_key.pem -inform pem -nocrypt -out private_key.der -outform der
[/code]
まず普通にpem形式で出力、そのあとJavaで扱えるようder形式ファイルも出力。上のコマンドはサーバ上で走らせたもの。Windowsマシンでやる場合はオプション指定が微妙に違うらしいので注意。詳しくは↑の参考サイトを。

次にRSA公開鍵をUWP(C++/CX)で扱える形にする。ここでかなりの試行錯誤。でもまさに欲しかった情報を書いてくださっているブログを発見。「Base64エンコードしたmodulusとpublicExponent」つまり文字列を二つ公開鍵から生成すれば良さそうだと理解。以下のJavaコードを書く。
[code language=”java”]
//定数
private static final String CIPHER_ALGORITHM = "RSA";
private static final String CIPHER_MODE = CIPHER_ALGORITHM + "/ECB/PKCS1PADDING";
//まずキーファイル読み込み
private static byte[] readKeyFile(String path) throws IOException{
byte[] data = null;
FileInputStream in = new FileInputStream(path);
data = new byte[in.available()];
in.read(data);
in.close();
return data;
}
//ファイルから公開鍵生成
private static RSAPublicKey gen_publicKey_RSA(String publicKeyPath_RSA) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException{
byte[] keyData = readKeyFile(publicKeyPath_RSA);
KeySpec keySpec = new X509EncodedKeySpec(keyData);
KeyFactory keyFactory = KeyFactory.getInstance(CIPHER_ALGORITHM);
Key publicKey = keyFactory.generatePublic(keySpec);
return (RSAPublicKey)publicKey;
}
//↑のブログを参考に先頭の符号ビットを削る処理
public static byte[] stripLeadingZeros(byte[] values) {
if((values.length > 1) && (0x00 == values[0])){
byte[] r = new byte[values.length – 1];
System.arraycopy(values, 1, r, 0, values.length – 1);
return r;
}else{
return values;
}
}
//最後に文字列吐き出す処理。
private static void gen_modulus_and_pe(RSAPublicKey publickey){
byte[] modulesBytes = Utils.stripLeadingZeros(publickey.getModulus().toByteArray());
String modules = Base64.encodeBase64String(modulesBytes);
String pe = Base64.encodeBase64String(publickey.getPublicExponent().toByteArray());
System.out.println("——————————————");
System.out.println(modules);
System.out.println(pe);
System.out.println("——————————————");
}
[/code]

ここでいちいちファイルアクセスするのもどうなのと思ったので、Java側でも秘密鍵/公開鍵ともに文字列データとして扱えないかと試行錯誤開始。
stackoverflowを徘徊してると「秘密鍵の文字列化なんてダメ!絶対!!」的なレスがたくさん見られるのだけど、イマイチ意味が分からない。だってpemファイルも平文のファイルでしょ。「秘密鍵データにprivate_key.pemなんてわかりやすいファイル名つけて、中身は平文で保持」と「秘密鍵データをjarファイルの中に文字列定数として(今回は結局16進文字列にした)保持」の二つのやり方にセキュリテイ上の優劣があるのだろうか。
そう思って文字列化処理実装。参考
[code language=”java”]
//文字列化
private static String KeyData_toHexString(String path) throws IOException{
byte[] keyData = readKeyFile(path);
return toHexString(keyData);

}
private static String toHexString(byte[] data) {
StringBuilder buf = new StringBuilder();
for (byte d : data) {
buf.append(String.format("%02X", d));
}
return buf.toString();
}
//文字列からキー生成。
private static RSAPrivateKey gen_privateKey_RSA_from_hexString(String hexString) throws InvalidKeySpecException, NoSuchAlgorithmException{
byte[] keyData = hexToByte(hexString);
KeySpec keySpec = new PKCS8EncodedKeySpec(keyData);
KeyFactory keyFactory = KeyFactory.getInstance(CIPHER_ALGORITHM);
return (RSAPrivateKey)keyFactory.generatePrivate(keySpec);
}
private static RSAPublicKey gen_publicKey_RSA_from_hexString(String hexString) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException{
byte[] keyData = hexToByte(hexString);
KeySpec keySpec = new X509EncodedKeySpec(keyData);
KeyFactory keyFactory = KeyFactory.getInstance(CIPHER_ALGORITHM);
Key publicKey = keyFactory.generatePublic(keySpec);
return (RSAPublicKey)publicKey;
}
private static byte[] hexToByte(String hexString){
int size = hexString.length() / 2;
byte[] bytes = new byte[size];
for(int i = 0;i<size;i++){
String tmp = hexString.substring(i * 2,i * 2 + 2);
bytes[i] = (byte) Integer.parseInt(tmp,16);
}
return bytes;
}
[/code]
以降はJava側ではこれで生成した秘密鍵データと公開鍵データの16進文字列を定数として埋め込み利用する。正直、pemファイルでもderファイルでも何でも良かったんだけど、今後色んな環境でテストしていく中で毎回ファイルのパス指定を間違えて躓きそうだったのでこうしました。暗号化/復号化処理に必須の処理ではないです。

次はUWP(C++/CX)側の処理。ここから情報が少なすぎて大変だった。問題は暗号化処理のサンプルコードで頻出する「System.Security.Cryptography」名前空間がUWPでは使えないこと。「Windows::Security::Cryptography」使えとのこと。結果web空間上に山ほどあるサンプルコードが参考にならなくなる。とりあえず↑の参考ブログで使ってたRSACryptoServiceProviderはWindows::Security::Cryptographyにはないので、では何を使うのか?とググったりコードアシストで色々試したり。結果CryptographicBuffer.DecodeFromBase64Stringあたりを使えば行けそうとの感触を得てAPIドキュメントみるも、パラメータは「Base64 encoded input string.」一つだけ。え、modulusとpublicExponentの二つじゃないの?さらにググりまくった結果、回答発見。要するに「modulusとpublicExponentからcspBlobString生成しろ。その処理はUWP(C++/CX)使わなくても構わん。つまりSystem.Security.Cryptography名前空間使って良し」とのこと。よって次の処理部分だけC#でやってみる。初めてのC#。
[code language=”csharp”]
static String gen_cspBlobString()
{
String MODULUS = "Javaで生成した文字列";
String PE = "Javaで生成した文字列";

RSAParameters parameters = new RSAParameters();
parameters.Modulus = System.Convert.FromBase64String(MODULUS);
parameters.Exponent = System.Convert.FromBase64String(PE);

System.Security.Cryptography.RSACryptoServiceProvider rsa = new System.Security.Cryptography.RSACryptoServiceProvider();
rsa.ImportParameters(parameters);
String cspBlobString = Convert.ToBase64String(rsa.ExportCspBlob(false));

return cspBlobString;
}
[/code]
これでできた文字列を使い、以降はUWP(C++/CX)でRSA暗号化処理。
[code language=”cpp”]
using namespace Windows::Storage::Streams;
using namespace Windows::Security::Cryptography;
String^ TEST_RSA_AES::MainPage::encryption_RSA(String^ target)
{
String^ result = "";
String^ cspBlobString = "↑のC#スクリプトで吐き出した文字列";
IBuffer^ keyBlob = CryptographicBuffer::DecodeFromBase64String(cspBlobString);
Core::AsymmetricKeyAlgorithmProvider^ rsa = Core::AsymmetricKeyAlgorithmProvider::OpenAlgorithm(Core::AsymmetricAlgorithmNames::RsaPkcs1);
Core::CryptographicKey^ key = rsa->ImportPublicKey(keyBlob, Core::CryptographicPublicKeyBlobType::Capi1PublicKey);
IBuffer^ plainBuffer = CryptographicBuffer::ConvertStringToBinary(target, BinaryStringEncoding::Utf8);
IBuffer^ encryptedBuffer = Core::CryptographicEngine::Encrypt(key, plainBuffer, nullptr);

result = CryptographicBuffer::EncodeToBase64String(encryptedBuffer);

return result;
}
[/code]
Java側で復号。web上の情報も多くてすぐ処理が組めるw
[code language=”java”]
private static String decryptRSA(String encrypted_base64) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException{
byte[] src = Base64.decodeBase64(encrypted_base64);
Cipher cipher = Cipher.getInstance("RSA");
byte[] keyData = Utils.hexToByte(privarteKeyText_RSA);
KeySpec keySpec = new PKCS8EncodedKeySpec(keyData);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Key privateKey = keyFactory.generatePrivate(keySpec);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] resultBytes = cipher.doFinal(src);
String result = new String(resultBytes,"UTF-8");
return result;
}
[/code]
暗号化の定石、「平文の暗号化はAES共通鍵方式で行い、添付する共通鍵をRSA方式で暗号化する」処理を実装。試しに長めの平文をRSA暗号化処理させてみたら、開発に使ってるi7搭載機でも引っかかる。
何より今回の暗号化処理は最終的にはwebAPI実装の一部だから、POSTアクセスなら文字数制限ないけれどもあまりに長いクエリになるのは困る。以下UWP(C++/CX)でAES暗号化処理
[code language=”cpp”]
Platform::Collections::Vector<Platform::String^>^ TEST_RSA_AES::MainPage::encryption_AES(Platform::String ^ target)
{

String^ algoName = Core::SymmetricAlgorithmNames::AesCbcPkcs7;
String^ pass = "パスワード";

Core::SymmetricKeyAlgorithmProvider^ algo = Core::SymmetricKeyAlgorithmProvider::OpenAlgorithm(algoName);

IBuffer^ salt = CryptographicBuffer::GenerateRandom(32);
IBuffer^ textBuffer = CryptographicBuffer::ConvertStringToBinary(target, BinaryStringEncoding::Utf8);
IBuffer^ digestBuffer = gen_digest(algo, pass, salt);
IBuffer^ ivBuffer = CryptographicBuffer::GenerateRandom(algo->BlockLength);

IBuffer^ encryptedBuffer = Core::CryptographicEngine::Encrypt(algo->CreateSymmetricKey(digestBuffer), textBuffer, ivBuffer);

Platform::Collections::Vector<String^>^ result = ref new Platform::Collections::Vector<String^>;
String^ encrypted = CryptographicBuffer::EncodeToBase64String(encryptedBuffer);
result->Append(encrypted);
OutputDebugString(("encrypted: " + encrypted + L"\r\n")->Data());
String^ iv = CryptographicBuffer::EncodeToBase64String(ivBuffer);
result->Append(iv);
OutputDebugString(("iv: " + iv + L"\r\n")->Data());
String^ digest = encryption_RSA(CryptographicBuffer::EncodeToBase64String(digestBuffer));
result->Append(digest);
OutputDebugString(("digest: " + digest + L"\r\n")->Data());
return result;
}

Windows::Storage::Streams::IBuffer ^ TEST_RSA_AES::MainPage::gen_digest(Windows::Security::Cryptography::Core::SymmetricKeyAlgorithmProvider^ algo, Platform::String ^ pass, Windows::Storage::Streams::IBuffer ^ salt)
{
uint32 SALT_ITERATION_COUNT = 10000;
Core::KeyDerivationAlgorithmProvider^ pbkdf2 = Core::KeyDerivationAlgorithmProvider::OpenAlgorithm(Core::KeyDerivationAlgorithmNames::Pbkdf2Sha256);
IBuffer^ passBuffer = CryptographicBuffer::ConvertStringToBinary(pass, BinaryStringEncoding::Utf8);
Core::CryptographicKey^ key = pbkdf2->CreateKey(passBuffer);
Core::KeyDerivationParameters^ parameters = Core::KeyDerivationParameters::BuildForPbkdf2(salt, SALT_ITERATION_COUNT);
return Core::CryptographicEngine::DeriveKeyMaterial(key, parameters, 32);

}

[/code]
結果、暗号化データのbase64文字列、ivのbase64文字列、digestのbase64文字列をRSA暗号化した文字列が生成される。
それをJava側で復号。
[code language=”java”]
private static String decryptAES(String encrypted_base64,String iv_base64,String digest_base64) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IOException, IllegalBlockSizeException, BadPaddingException{
byte[] digest = Base64.decodeBase64(digest_base64);
byte[] iv = Base64.decodeBase64(iv_base64);
byte[] encryptedAES = Base64.decodeBase64(encrypted_base64);
SecretKey secretKey = new SecretKeySpec(digest, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey,new IvParameterSpec(iv));

ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(cipher.update(encryptedAES));
bos.write(cipher.doFinal());
byte[] bytes = bos.toByteArray();
return new String(bytes,0,bytes.length,"UTF-8");
}
//こんな感じで使う
String dec = decryptAES(ENCRYPTED_TEXT_AES64, IV_64, decryptRSA(DIGEST_RSA64));
System.out.println(dec);
[/code]
おそらく初回はこのAES復号処理はInvalidKeyExceptionで落ちる。JavaにはAES鍵長の制限があるらしい。参考
以上、今回実装した処理のすべて。このエントリ書くのに2時間かかったw リンクした参考先サイトのすべての皆さんに感謝。こんなコピペプログラマのやっつけで書いてるコードでない、理論に裏付けされた美しいコードが並んでます。暗号化について調べていてこのエントリにたどり着いた人がもしいたら、是非リンク先へ行ってみてください。

≒ jawiki/latest 20161220/ のページ数の件>2124636

約5000レコード増加。ありがたい。

次もUWPアプリしこしこ頑張ってるわけですけども、サーバがトラブル頻発でその対処に時間を取られることが多々あり。
前のエントリでclamavの件は書いたんだけど、それ以外にもっと重大なトラブルもあり、1件は対処に数日取られるレベルでした。
エントリに書き残さないのは、ライトア〇〇ルトサイトのサーバだったためで、それもxv〇〇eosのスクレイピングスクリプトの不具合なので細かいことは公開できず、メモ書きを残してあるだけになってます。
しかし何が困ったってモダンJavaScriptがわかってないから、コードを目で追っても解読できない部分がでてきちゃう。xv〇〇eosさんはコードもhtmlも泥臭いところに好感持ってたのに、置いてかれるようで寂しい。

俺もnode.jsとかAngular2とか学習に取り掛かってみようとは何回かしてるんだけど、いつも情報収集の段階で挫折。
クライアントスクリプトのほうはどんどん軽くしていくのが好みになっちゃって、一年前までJQueryやBootstrap使って「パチンコ台のような」サイト作って喜んでたのがウソのような現在、nodeとかの情報探り始めても「ライブラリ」って言葉があるたびに気力減退して中断。とにかく今はブラウザ実装のJavaScriptだけでできることをやりたい感じ。おかげで最近作るサイトはPageSpeedInsightsの評価はすごくいい。
サーバサイドは元々Javaの静的型付けが好きなこともあり、コンパイラ言語からわざわざインタープリタ言語に代えるメリットがどうしてもわからない。もうしばらく様子見だなぁ。

≒ 【未解決】clamavでWin.Trojan.Toa検知

タイトル通り。一昨日二つのサーバ(借りてる先は別会社)同時に検知。で、FireFox関連の「omni.ja」ってファイルと、threeLessonのwarファイルが検出されて削除される。
色々ググったところ誤検知らしいとのこと。大丈夫だと判断し、Firefoxは再インストール、warファイルは再デプロイ。clamavでスキャンしても検出されず。良かった良かった。
ところが、昨日また「Virus Found in beingtested.jp」とのメールが。「/tomcat/webapps/threeLesson.war: Win.Trojan.Toa-5366523-0 FOUND」再び。それは良い。わかった。
でも「/jre/lib/ext/nashorn.jar: Win.Trojan.Toa-5370166-0 FOUND」には参った。jdk壊すのは無しだよー。更にこのサイトと違うほうのサーバでは全warファイルが削除されてる。全部Tomcatで動いてるのでご丁寧にもROOT.warまで消されてサイトまるごと404。くそー。
速攻でやった対処はウィルススキャンスクリプトから「–remove」消す。この野郎!参考
そしてこのブログを書いている。なぜか。jdkのRPMをアップロードするのにサーバ1台当たり30分かかるのです。jdkのRPMインストールは上書きでOKらしいので(確定情報ではないです。30分後にわかるけどw)この後試す。そしてFirefox再インストール(単純にyum removeしてからyum installして昨日は上手く行った)して、warファイル全部を再デプロイしたら俺の中では今回の件、対処完了とする。アップロード待ち時間を潰すためのブログエントリは以上で終了!あとでjdkの再インストールの件は追記します。
でも今回久しぶりにググってて笑ったのはこのページ。clamavの誤検知の件が最後の方では「Linuxにはウィルス対策いらん」との大合唱で結局ウィンドウズdisってるだけの内容になってるw

追記
「sudo rpm -ivh jdk-8u112-linux-x64.rpm」打つと解凍してるのは見えるんだけど、その後何もなく静かに終わる。何かconfirmとかなかったっけ?心配なのでここを参考にいったんアンインストールして再度インストール。手持ちのjar走らせてみて大丈夫だったので今度こそ以上。