≒ 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 リンクした参考先サイトのすべての皆さんに感謝。こんなコピペプログラマのやっつけで書いてるコードでない、理論に裏付けされた美しいコードが並んでます。暗号化について調べていてこのエントリにたどり着いた人がもしいたら、是非リンク先へ行ってみてください。