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

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

まずはRSA鍵生成。最初はJavaで生成するを見つけてその通りやろうとして、鍵データの保存/呼び出し方法で四苦八苦。
でもよく考えたらこの部分は普通にOpenSSL使えばいいんじゃないか?情報も多いし。参考

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

まず普通にpem形式で出力、そのあとJavaで扱えるようder形式ファイルも出力。上のコマンドはサーバ上で走らせたもの。Windowsマシンでやる場合はオプション指定が微妙に違うらしいので注意。詳しくは↑の参考サイトを。

次にRSA公開鍵をUWP(C++/CX)で扱える形にする。ここでかなりの試行錯誤。でもまさに欲しかった情報を書いてくださっているブログを発見。「Base64エンコードしたmodulusとpublicExponent」つまり文字列を二つ公開鍵から生成すれば良さそうだと理解。以下の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("------------------------------------------");
}

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

//文字列化
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;
	}

以降は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#。

 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;
        }

これでできた文字列を使い、以降はUWP(C++/CX)でRSA暗号化処理。

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;
}

Java側で復号。web上の情報も多くてすぐ処理が組めるw

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;
	}

暗号化の定石、「平文の暗号化はAES共通鍵方式で行い、添付する共通鍵をRSA方式で暗号化する」処理を実装。試しに長めの平文をRSA暗号化処理させてみたら、開発に使ってるi7搭載機でも引っかかる。
何より今回の暗号化処理は最終的にはwebAPI実装の一部だから、POSTアクセスなら文字数制限ないけれどもあまりに長いクエリになるのは困る。以下UWP(C++/CX)でAES暗号化処理

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);

}

結果、暗号化データのbase64文字列、ivのbase64文字列、digestのbase64文字列をRSA暗号化した文字列が生成される。
それを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);

おそらく初回はこのAES復号処理はInvalidKeyExceptionで落ちる。JavaにはAES鍵長の制限があるらしい。参考
以上、今回実装した処理のすべて。このエントリ書くのに2時間かかったw リンクした参考先サイトのすべての皆さんに感謝。こんなコピペプログラマのやっつけで書いてるコードでない、理論に裏付けされた美しいコードが並んでます。暗号化について調べていてこのエントリにたどり着いた人がもしいたら、是非リンク先へ行ってみてください。

“ANDROID.PERMISSION.CAMERA”

驚いたー。

今さらですけど最近Androidで遊び始めました。

どこの情報見ても「eclipse+ADTはオワ…」とのことでしたので空いてたSSD1個専用に設置してAndroidStudio入れたわけですよ。

当然使いにくい。いまいちコード補完がしっくりこない。俺syso多用するし。でも慣れるしかないと使ってたわけですよ。

ほんで、半日はまったのがこれ。

<uses-permission android:name=”ANDROID.PERMISSION.CAMERA” />

カメラ使うなら当然の1行。きっちりコード補完で出るし、やっぱりASにしてよかったと思うわけです。

しかし、全然動かない。

java.lang.RuntimeException: Fail to connect to camera service

未だにcamera2じゃなくてCamera使ってるからか、とか、surfaceViewのxmlへの設置ってしなくて良いの?とか、よく分かってないから全方位手探り状態。よくlogcat見ると

D/Camera﹕ open(int cameraId) application != null

なので全アプリ終了、再起動もしましたよ。でもだめ。

半日netをさまよった挙句、「なんか違う」とおもって試しに

<uses-permission android:name="android.permission.CAMERA" />

一部小文字にしたらちゃんと動いた。AS怖い。

未解決)Tomcat環境へのapplet配置

servletは少しづつ勉強してってるんですけど、ちょっとappletをどんな風に配置するのか知りたくて、試しにapplet一個置いただけのページを作成してみる。大はまり。

1)appletの置き場所がわからない

Tomcat Webアプリケーションマネージャ最高!セキュリティの穴ふさぐのめんどくさいけど、その手間を補って余りある使いやすさw まぁ慣れたら使わなくなるらしいけどね。

で、warファイルはeclipseでそのままシームレスにエクスポートできるのが最高!ってやってるんだけど、appletの置き場がわからない。普通にclassとして新規作成するとWEB-INFディレクトリ配下に置かれてしまう。

試行錯誤したんだけどこれは結局まだわかってない。

2)WEB-INFディレクトリの仕様

これはservlet内部からしかアクセス出来ない。htmlなどからパス指定をしても一切見えないのです。色んなページ漁ってみたけどアクセス方法がない。でも探してるうちに、こういうふうにフォルダアクセスも仕組み自体で完全に制御してるところがTomcatの良い点だと理解できた。

3) 1)2)を踏まえての問題

結局WEB-INFより上=ページのルートはアクセスできるんだから、そこにフォルダ作ってapplet置いてhtml指定でアクセスするのが一番簡単な解決法。でもそれだとwarファイル展開→できた構造の中にapplet放り込む、って手順になってなんかエレガントじゃない。warファイル自体の(再)配備も上手く行かなくなるし。そこはマネージャ使って簡単にできる状態から後退するのは何かイヤ。

4)context.xml

関係ない。ページのルートから外れた場所に置いたフォルダ(ディレクトリ)を認識させるために設定するものなんだけど、いまいちよくわからなくて3時間くらい試行錯誤。でもよく考えたらhtml(=クライアントサイド)から認識できるようにすれば良いだけなんだから、context.xmlの設定は必要無い。おかげでTomcatのディレクトリ構造の勉強にはなったけど。

5)今のところ

webapps直下にフォルダ(openSauce w)作ってそこにぶちこんだ。ページからは階層上ればアクセスできる。appletとか画像とかのデータは隠す必要無いんだし、今後他のページ作った時もここなら同じ手順でアクセスできるし。

以上。何か間違ってるような気がしないでもないので書き残し。しかしjsp良いです。できたのはこのページなんですけど、appletをjsp:pluginタグで埋め込んだら勝手にObjectタグとembedタグに書き分けてくれる。先日の苦労は何だったんだw

追記)

jsp:pluginタグの代替コメント指定<jsp:fallback>タグだとnoembedタグでの表記に変換されちゃうな。objectタグの方に適用してくれないと認識されないんですけど。