タグ『 UWP(C++/CX) 』

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

≒ TinyMediaPlayerの件

公開しました。
TinyMediaPlayer
制作に約3カ月もかかった。その割にはただのメディアプレイヤーなんですけど。

制作に3カ月もかかったのは初めてのUWP(C++/CX)コーディングだったという俺の能力不足がでかいんだけど、今回申請から公開まで1週間弱かかったのはびっくりした。
まず認定結果が出るまで3日かかった。そんで1回目は不合格。メディアデータにアクセスするアプリの場合プライバシーポリシー明示が必要らしい。事前にそういう基本的な情報がネットで引っかからないところがUWPアプリの寂しさ。
HTMLぺら一のプライバシーポリシー掲載サイト作ってリンクを所定の位置に記入して再申請。今度は2日で認定処理終了。合格。デベロッパーセンターの表示が「公開中」になるもストアに出てこない。良く見ると「ストア情報」とかいうのが「保留」になってる。ググってみると「公開中」という日本語表記はen表示だと「公開準備中」という内容に該当するらしい。2日経って「ストア内」という表記に代わって無事公開状態になった。最初の不合格別にしても、Androidに慣れた身からすると、すごく時間かかるんだなと思った。もちろん全く文句はない。MicroSoftさんの慎重さにはすごく好感が持てる。その慎重さとオープンソース化による諸々の更新がバッティングしちゃって、APIなどのドキュメントがカオス状態なのは何とかしてほしいと思うけども、アプリの「名前予約」ってシステムとかは良い慎重さだと思う。同名アプリがないってのはやっぱりうれしい。

以上、公開報告。良かったら使ってみてください。広告もネットアクセスも無いので軽いはずです。というかそれだけを目的に作りました。よろしく。

≒ 【未解決】UWPのExtendViewIntoTitleBar問題

タイトルバーにアイコン表示するだけで2時間以上格闘。
でも結局やめた。理由は各種サイズをハードコードする例しか見つからず、その通り書くとウィンドウサイズが違う時(+変わった時)の挙動が不安定になりそうだったから。俺はデザインの数値の決め打ちは基本的に嫌い。”Auto”って設定も出来そうだったけど、iconのサイズは結局決め打ちになりそうだったし。良い感じの情報もあったけど、またBehaviorかよ! なんでUWPはタイトルバーにアイコン1個表示するだけでそんなにコード増えるんだよ。

問題はその後。2時間以上かけて書いたコード全部消して、再ビルド。タイトルバーが表示されない。タイトルバーを何か操作してるコードは一切ない。プロジェクト全体検索してもtitlebarの文字は無い。気持ち悪いのはOnFileActivated経由だと普通に表示される。OnFileActivatedの中で何かしてるコードはない。もちろんソリューションのクリーン、アプリのアンインストール、VisualStudioの再起動、PCの再起動、全部試した。
で、結局OnLaunchedの先頭に
[code language=”cpp”]
Windows::ApplicationModel::Core::CoreApplication::GetCurrentView()->TitleBar->ExtendViewIntoTitleBar = false;
[/code]
の1行を入れると表示される。先頭だから、後続で実行される俺の書いたコードの中ではExtendViewIntoTitleBarをtrueに設定するようなコードはないってこと。うーん。おまじないみたいなコード書くのは嫌だなぁ。以上、びっくりしたので覚え書き。