この記事のURL

http://www.dango-itimi.com/blog/archives/2011/001093.html


FLASH tips 暗号化調査2 : as3crypto ←→ PyCrypto : DES, AES, RSA

前回の「暗号化調査」記事の続きです。
Action Script 3(AS3), python 双方向に暗号化通信を目的とした、共通鍵暗号方式(DES, AES)と公開鍵暗号方式(RSA)のプログラミングに関する調査が完了しました。

環境

AS3 側は as3crypto という暗号化用ライブラリを用いています。as3crypto.swc バージョン 1.3 と、svnリポジトリにある最新バージョンのソースファイル一式 両方で検証しました。

python 側は Google App Engine がデフォルトでサポートしている PyCrypto という暗号化ライブラリを用いました。python のバージョンは 2.5.2, PyCrypto のバージョンは 2.0.1 となります。最近 GAE でサポートされた python 2.7, PyCrypto 2.3 でも動作検証を行っています。

 as3crypto
 http://code.google.com/p/as3crypto/

 PyCrypto
 https://www.dlitz.net/software/pycrypto/

 windows 用 PyCrypto 2.0.1 ダウンロード URL
 http://www.voidspace.org.uk/python/modules.shtml#pycrypto

調査結果

AS3, python 共にネット上にサンプルソースが載っていた、共通鍵暗号方式(DES, AES)と公開鍵暗号方式(RSA)についてプログラミング調査を行ないました。共通鍵暗号方式の AS3, python 双方向暗号化に対しては、特に問題は発生せず、意図通りの動作となりました。

しかし公開鍵暗号方式(RSA)で問題が発生。AS3 側で暗号化したデータを python 側で復号する事は成功しましたが、python 側で暗号化したデータを AS3 側で復号する事ができませんでした。as3crypto の復号処理でエラーが発生してしまいます。

何が問題か as3crypto, PyCrypto 両方共に調査を行なってみましたが、ネット上の資料はあまり多くは見つからず、はっきりとした原因はわかりませんでした。どうも、RSA 暗号化方式にはパディング処理という物がいくつかあり、PyCrypto の採用しているパディング処理に対し as3crypto の復号処理が対応していないっぽい、という感じがします。それらしきキーワードが書かれていた英語の質問フォーラムの記述を断片的につなげてみただけなので、めちゃくちゃ言ってしまっているかもしれません。PyCrypto の RSA 暗号処理は PKCS#1 v1.5?方式で、as3crypto ではそれは解析できないとかどうとか、まだよく分かっていません。

「環境」の項目に記述した as3crypto, PyCrypto 二種類のバージョンで検証を行いましたが、動作の違いはなく問題は解決しませんでした。

as3crypto の復号処理で、「どういう復号処理を行うか」を指定できる箇所(RSAKey.decrypto メソッドの第四引数)があり、そこに pycrypto で暗号化したデータを復号するための独自のメソッドを指定すれば問題は解決しそうではあります。しかし別途、復号アルゴリズムの勉強を行わなくてはならなそうなので、python → AS3 への RSA 暗号化通信に関しては保留としたいと思います。

参考サイト

 ■ AS3
 AS3で暗号/復号化 : suz-lab
 AS3で暗号/復号化(RSA版) : suz-lab
 RSA暗号化して通信経路途中での改ざんを防止 : KYUCON*BLOG
 
 ■ python
 Pythonでの暗号化/復号化(AEC-DES,RSA) : YutaKikuchiのTechBlog
 PyCryptoで暗号化する : Pythonメモ

 ■ ツール
 AS3 Crypto Demo page

サンプルソースコード

暗号化・復号用クラスと検証用クラスのソース一式は以下となります。AS3, python 共にロジックはほぼ同じです。

暗号化対象の文字列を暗号化し Base64エンコードするまでを encryptメソッドで行い、Base64エンコードされた暗号化文字列を 暗号化されていない文字列にまで復号するまでの処理を decrypt メソッドで行なっています。

AS3 : DES, AES

AS3 で共通鍵暗号方式(DES, AES)を行うためのクラス SymmetricCrypter

package crypto_test {

	import com.hurlant.crypto.symmetric.AESKey;
	import com.hurlant.crypto.symmetric.DESKey;
	import com.hurlant.util.Base64;
	import com.hurlant.crypto.symmetric.ISymmetricKey;
	import com.hurlant.util.Hex;
	import flash.utils.ByteArray;
	
	public class SymmetricCrypter {

		private var crypter:ISymmetricKey;

		public static function createDESCrypter(key:String):SymmetricCrypter {
			
			return new SymmetricCrypter(DESKey, key);
		}
		public static function createAESCrypter(key:String):SymmetricCrypter {
			
			return new SymmetricCrypter(AESKey, key);
		}
		public function SymmetricCrypter(crypterClass:Class, key:String) {
			
			var keyBin:ByteArray = Hex.toArray(Hex.fromString(key));
			crypter = new crypterClass(keyBin);
		}
		
		public function encrypt(plainText:String):String {
			
			var textBin:ByteArray = Hex.toArray(Hex.fromString(plainText));
			crypter.encrypt(textBin);
			
			var cipherText:String = Base64.encodeByteArray(textBin);
			return cipherText;
		}
		public function decrypt(cipherText:String):String {
			
			var decodedTextBin:ByteArray = Base64.decodeToByteArray(cipherText);
			crypter.decrypt(decodedTextBin);	
			
			var decryptedText:String = Hex.toString(Hex.fromArray(decodedTextBin));
			return decryptedText;
		}
	}
}

SymmetricCrypter クラスの検証用クラス

package crypto_test {
	
	public class SymmetricCryptoTester {

		public function test():void {
			
			testDES();
			testAES();
		}
		private function testDES():void {
			
			trace("--- testDES ---");
			
			testCmn(SymmetricCrypter.createDESCrypter("abcdefgh"), "ohayou12");
		}
		private function testAES():void {
			
			trace("--- testAES ---");
			
			testCmn(SymmetricCrypter.createAESCrypter("aaaaaaaabbbbbbbb"), "ohayou12konbanwa");
		}

		private function testCmn(crypter:SymmetricCrypter, plainText:String):void {
			
			var cipherText:String = crypter.encrypt(plainText);
			trace('cipherText: ' + (cipherText));
			
			var decryptedText:String = crypter.decrypt(cipherText);
			trace('decryptedText: ' + (decryptedText));
		}
	}
}

SymmetricCryptoTester の実行結果

--- testDES ---
cipherText: jKbIR+rbJ8w=
decryptedText: ohayou12
--- testAES ---
cipherText: 96JhvLji/Bi7as61vYAO2g==
decryptedText: ohayou12konbanwa

python : DES, AES

python で共通鍵暗号方式(DES, AES)を行うためのクラス SymmetricCrypter

import Crypto.Cipher.DES as DES
import Crypto.Cipher.AES as AES
import base64

def createDESCrypter(key):
	
	return SymmetricCrypter(DES, key)

def createAESCrypter(key):
	
	return SymmetricCrypter(AES, key)

class SymmetricCrypter:
	
	def __init__(self, crypterClass, key):
		
		self.__crypter = crypterClass.new(key, crypterClass.MODE_ECB)

	def encrypt(self, plainText):
		
		encryptedTextBin = self.__crypter.encrypt(plainText)
		encodedText = base64.b64encode(encryptedTextBin)
		return encodedText

	def decrypt(self, cipherText):

		decodedTextBin = base64.b64decode(cipherText)
		decryptedText = self.__crypter.decrypt(decodedTextBin)
		return decryptedText

SymmetricCrypter クラスの検証用クラス

import SymmetricCrypter

class SymmetricCryptoTester:
	
	def test(self):
		
		self.__testDES()
		self.__testAES()

	def __testDES(self):
		
		print "--- testDES ---"
		
		self.__testCmn(
			SymmetricCrypter.createDESCrypter("abcdefgh"), "ohayou12")

	def __testAES(self):

		print "--- testAES ---"
		
		self.__testCmn(
			SymmetricCrypter.createAESCrypter("aaaaaaaabbbbbbbb"), "ohayou12konbanwa")
	
	def __testCmn(self, crypter, plainText):
		
		cipherText = crypter.encrypt(plainText)
		print "cipherText: " + cipherText
		
		decryptedText = crypter.decrypt(cipherText)
		print "decryptedText: " + decryptedText

if __name__ == '__main__':
	tester = SymmetricCryptoTester()
	tester.test()

SymmetricCryptoTester の実行結果

--- testDES ---
cipherText: jKbIR+rbJ8w=
decryptedText: ohayou12
--- testAES ---
cipherText: 96JhvLji/Bi7as61vYAO2g==
decryptedText: ohayou12konbanwa

AS3, python ともに実行結果は同じとなります。よって双方向暗号化処理に問題はありません。

AS3 : RSA

AS3 で公開鍵暗号方式(RSA)を行うためのクラス RSACrypter

package crypto_test {

	import com.hurlant.util.Base64;
	import com.hurlant.crypto.rsa.RSAKey;
	import com.hurlant.util.Hex;
	import flash.utils.ByteArray;
	
	public class RSACrypter {

		private var PUBLIC_MODULUS:String;
		private var PUBLIC_EXPONENT:String;
		private var PRIVATE_EXPONENT:String;
		private var P:String;
		private var Q:String;
		private var DMP1:String;
		private var DMQ1:String;
		private var IQMP:String;
		private var encrypter:RSAKey;
		private var decrypter:RSAKey;

		public function RSACrypter(
			PUBLIC_MODULUS:String, 
			PUBLIC_EXPONENT:String, 
			PRIVATE_EXPONENT:String = null, 
			P:String = null, 
			Q:String = null, 
			DMP1:String = null, DMQ1:String = null, IQMP:String = null
		) {
			this.IQMP = IQMP;
			this.DMQ1 = DMQ1;
			this.DMP1 = DMP1;
			this.Q = Q;
			this.P = P;
			this.PRIVATE_EXPONENT = PRIVATE_EXPONENT;
			this.PUBLIC_EXPONENT = PUBLIC_EXPONENT;
			this.PUBLIC_MODULUS = PUBLIC_MODULUS;
		}
		public function createEncrypter():void {
			
			encrypter = RSAKey.parsePublicKey(PUBLIC_MODULUS, PUBLIC_EXPONENT);
		}
		public function createDecrypter():void {
			
			decrypter = RSAKey.parsePrivateKey(PUBLIC_MODULUS, PUBLIC_EXPONENT, PRIVATE_EXPONENT, P, Q, DMP1, DMQ1, IQMP);
		}

		public function encrypt(plainText:String):String {
			
			var textBin:ByteArray = Hex.toArray(Hex.fromString(plainText));
			var encryptTextBin:ByteArray = new ByteArray();	
			
			encrypter.encrypt(textBin, encryptTextBin, textBin.length);	
			
			var cipherText:String = Base64.encodeByteArray(encryptTextBin);
			return cipherText;
		}
		public function decrypt(cipherText:String):String {
			
			var decodedTextBin:ByteArray = Base64.decodeToByteArray(cipherText);
			var decryptedTextBin:ByteArray = new ByteArray();
			
			decrypter.decrypt(decodedTextBin, decryptedTextBin, decodedTextBin.length);
			
			var decryptedText:String = Hex.toString(Hex.fromArray(decryptedTextBin));
			return decryptedText;
		}
	}
}

RSACrypter クラスの検証用クラス

package crypto_test {

	public class RSACryptoTester {

		private var crypter:RSACrypter;
		
		public function RSACryptoTester() {
			
			crypter = new RSACrypter(
				"a1b7ee12d7f83208b7a3ad028b95b679a94a4fee56324e0232919680c7ca8d2d411c61246a86c3eea635043c80248ccee58cd1f1fd57f5a09ac03b8a6e0b8ac7",
				"10001",
				"952a74bc88dcf439ba398d01c602c8179c21616cf3b0dee784016a134b35f7d500d48037ef9132734bbb337fb1917de8a7a280d81f5b72c30ba4805f28d56a61",
				"fa1519c5586d0ced156aaef81d8f694cbcb8fd4b61fed35cbefdbd83c9e7d5a9",
				"a58b8e4bb37f10857d9652bf703d466dc876270d0a33ef16a0c0a328b5c9c2ef",
				"bdd35a7673c282f74b34698fa8507d6b642d3f0703617c6a8006f6dd9c5abfd1",
				"24fed92005cf21ac1372e2af31b01feb2cdf6abeb946fde97aa7524f28c11c35",
				"b9912750e685af9bca5bed7effb1f8ea459d87dd458a3e2046e691dcf9a898ed"
			);
			crypter.createEncrypter();
			crypter.createDecrypter();
		}

		public function test():void {
			
			test1();
			test2();
			test3();
		}
		private function test1():void {
			
			trace('--- test1 ---');
			
			var cipherText:String = crypter.encrypt("konbanwa");
			trace('cipherText: ' + (cipherText));
			
			var decryptedText:String = crypter.decrypt(cipherText);
			trace('decryptedText: ' + (decryptedText));
		}
		private function test2():void {
			
			trace('--- test2 ---');
			
			var cipherText:String = "l9T8qv5+ab+G7RdjwUZNctAtTG7SqZYEQyfUkvGU97mMUfWMC+pfIILm6LT86vwUAH0GsrKBg/6k7dy5WF9rsg==";
			var decryptedText:String = crypter.decrypt(cipherText);
			trace('decryptedText: ' + (decryptedText));
		}
		//error : pycrypto → ascrypto
		private function test3():void {
			
			trace('--- test3 ---');
			
			var cipherText:String = "nUB1y5xmDjTq2AsHI+EnetCNJuODedQWZ2Ojv41qPMEoI3/CaE/2KXSdoJvoourTmotCE9Xp07KnqU3VcedkhQ==";
			crypter.decrypt(cipherText);
		}

	}
}

RSACryptoTester クラスの説明です。

まず、AS3 Crypto Demo pageの Public Key タブにある Generate ボタンを押して、暗号化・復号に必要なキーを作成します。作成したキーを RSACrypter クラスのコンストラクタに設定します。コンストラクタの引数に全てのデータを設定していますが、実際は第三引数までで動作します。RSAKey.parsePrivateKey メソッドに第三引数まで指定した場合と、第三引数より後の引数全てを指定した場合とで動作がどう異なるのかは理解していません。「あらかじめ必要な計算を行なっておき、処理を高速化するため」と予想しておきます。

RSACryptoTester.test1 メソッドでは、文字列 konbanwa を暗号化・復号する検証を行なっています。

RSACryptoTester.test2 メソッドでは、AS3 Crypto Demo page の Public Key タブ Plain Text(Text) に「konbanwa」と入力し、Cipher Text(Base64)欄に出力された内容「l9T8qv5+ab+G7RdjwUZNctAtTG7SqZYEQyfUkvGU97mMUfWMC+pfIILm6LT86vwUAH0GsrKBg/6k7dy5WF9rsg==」を、復号する検証を行なっています。

RSACryptoTester.test3 メソッドでは、後述の python(PyCrypto) 側で「konbanwa」という文字列を暗号化した「nUB1y5xmDjTq2AsHI+EnetCNJuODedQWZ2Ojv41qPMEoI3/CaE/2KXSdoJvoourTmotCE9Xp07KnqU3VcedkhQ==」という文字列を復号する検証を行なっています。

RSACryptoTester クラスの実行結果は以下となります。

--- test1 ---
cipherText: oBFNV/IXcrGUhoCFg80MAF+zCDyUZeG6UAgnJwUT1t8cYMZoCouLdXC+Ea8qL1GmBZ8J22Rvp9YWuV3DRCyW5w==
decryptedText: konbanwa
--- test2 ---
decryptedText: konbanwa
--- test3 ---
PKCS#1 unpad: i=0, expected b[i]==2, got b[i]=6b
Error: Decrypt error - padding function returned null!
	at com.hurlant.crypto.rsa::RSAKey/_decrypt()
	at com.hurlant.crypto.rsa::RSAKey/decrypt()
	at crypto_test::RSACrypter/decrypt()
	at crypto_test::RSACryptoTester/test3()
	at crypto_test::RSACryptoTester/test()
	at crypto_test::CryptoTester()

RSACryptoTester.test3 メソッドの実行結果はエラーとなってしまいます。

python : RSA

python で公開鍵暗号方式(RSA)を行うためのクラス RSACrypter

import Crypto.PublicKey.RSA as RSA
import base64

class RSACrypter:
	
	def __init__(
		self, 
		PUBLIC_MODULUS, 
		PUBLIC_EXPONENT, 
		PRIVATE_EXPONENT = None, P = None, Q = None, IQMP = None
	):
		self.__n = long(PUBLIC_MODULUS, 16)
		self.__e = long(PUBLIC_EXPONENT, 16)
		
		if PRIVATE_EXPONENT: self.__d = long(PRIVATE_EXPONENT, 16)
		if P: self.__p = long(P, 16)
		if Q: self.__q = long(Q, 16)
		if IQMP: self.__u = long(IQMP, 16)
	
	def createEncrypter(self):
		
		self.__encrypter = RSA.construct((self.__n, self.__e))
	
	def createDecrypter(self):
		
		#self.__decrypter = RSA.construct((self.__n, self.__e, self.__d))
		self.__decrypter = RSA.construct((self.__n, self.__e, self.__d, self.__p, self.__q))
		#self.__decrypter = RSA.construct((self.__n, self.__e, self.__d, self.__p, self.__q, self.__u))
	
	def encrypt(self, plainText):
		
		encryptedTuple = self.__encrypter.encrypt(plainText, "")
		encodedText = base64.b64encode(encryptedTuple[0])
		return encodedText
	
	def decrypt(self, cipherText):
		
		decodedText = base64.b64decode(cipherText)
		decryptedText = self.__decrypter.decrypt(decodedText)
		return decryptedText

RSA.construct のタプル6番目の要素には AS3 Crypto Demo page Public Key タブの Generate ボタンで生成される 1/Q mod P の値を設定すればよいかと思いますが、自信がないのでひとまず5番目の要素までを設定できるようにしています。


RSACrypter クラスの検証用クラス

import RSACrypter

PUBLIC_MODULUS = "a1b7ee12d7f83208b7a3ad028b95b679a94a4fee56324e0232919680c7ca8d2d411c61246a86c3eea635043c80248ccee58cd1f1fd57f5a09ac03b8a6e0b8ac7"
PUBLIC_EXPONENT = "10001"
PRIVATE_EXPONENT = "952a74bc88dcf439ba398d01c602c8179c21616cf3b0dee784016a134b35f7d500d48037ef9132734bbb337fb1917de8a7a280d81f5b72c30ba4805f28d56a61"

P = "fa1519c5586d0ced156aaef81d8f694cbcb8fd4b61fed35cbefdbd83c9e7d5a9"
Q = "a58b8e4bb37f10857d9652bf703d466dc876270d0a33ef16a0c0a328b5c9c2ef"
DMP1 = "bdd35a7673c282f74b34698fa8507d6b642d3f0703617c6a8006f6dd9c5abfd1"
DMQ1 = "24fed92005cf21ac1372e2af31b01feb2cdf6abeb946fde97aa7524f28c11c35"
IQMP = "b9912750e685af9bca5bed7effb1f8ea459d87dd458a3e2046e691dcf9a898ed"
		
class RSACryptoTester:
	
	def __init__(self):
		
		self.__crypter = RSACrypter.RSACrypter(
			PUBLIC_MODULUS, PUBLIC_EXPONENT, PRIVATE_EXPONENT, P, Q, IQMP)
		
		self.__crypter.createEncrypter()
		self.__crypter.createDecrypter()
	
	def test(self):
		
		self.__test1()
		self.__test2()
		
	def __test1(self):
		
		print "--- test1 ---"
		
		cipherText = self.__crypter.encrypt("konbanwa")
		print cipherText
		
		decryptedText = self.__crypter.decrypt(cipherText)
		print decryptedText
	
	def __test2(self):
		
		print "--- test2 ---"
		
		cipherText = "l9T8qv5+ab+G7RdjwUZNctAtTG7SqZYEQyfUkvGU97mMUfWMC+pfIILm6LT86vwUAH0GsrKBg/6k7dy5WF9rsg=="
		decryptedText = self.__crypter.decrypt(cipherText)
		print decryptedText
		
		text = self.pkcs1_unpad(decryptedText)
		print text
	
	def pkcs1_unpad(self, text):
		
		import re
		m = re.match('\x02[^\x00]{8,}\x00(.*)', text)
		return m.group(1) if m else None

if __name__ == '__main__':
	tester = RSACryptoTester()
	tester.test()

RSACryptoTester クラスの説明です。AS3 版 RSACrypter と同じくコンストラクタには第三引数まで指定でも動作します。

RSACryptoTester.test1 メソッドでは、文字列 konbanwa を暗号化・復号する検証を行なっています。

RSACryptoTester.test2 メソッドでは、AS3 版 RSACryptoTester.test2 と同じ 暗号化文字列を復号する検証を行なっています。しかし意図通り復号されないため、補足処理(後述)を加えています。


RSACryptoTester クラスの実行結果は以下となります。

--- test1 ---
nUB1y5xmDjTq2AsHI+EnetCNJuODedQWZ2Ojv41qPMEoI3/CaE/2KXSdoJvoourTmotCE9Xp07KnqU3VcedkhQ==
konbanwa
--- test2 ---
バイナリ文字列konbanwa
konbanwa

RSACryptoTester.test1 メソッドで暗号化された文字列「nUB1y5xmDjTq2AsHI+EnetCNJuODedQWZ2Ojv41qPMEoI3/CaE/2KXSdoJvoourTmotCE9Xp07KnqU3VcedkhQ==」を、AS3版 RSACryptoTester.test3 メソッドで利用しています。python 側では復号可能ですが AS3 側では復号できないことがわかります。

RSACryptoTester.test2 メソッドの実行結果一行目はランダムなバイナリ文字列に加えて「konbanwa」という文字列が表示されます。as3crypto で暗号化した文字列を PyCrypto で復号しても、完全に復号できないことがわかります。対策として、RSACryptoTester.pkcs1_unpad メソッドでランダムなバイナリ文字列の削除を行なっています。

 pkcs1_unpad メソッド参考 URL
 http://kfalck.net/2011/03/07/decoding-pkcs1-padding-in-python

追記 2011/11/30)

上記サンプルソースコードですと、Pycrypto 側で DES 暗号化する文字列長は 8バイトの倍数ではなく 8バイト丁度でないと、as3crypto 側で復号が正常に行なえない事実が発覚しました。AES も同様に何か制限がかかってしまっているかもしれません。解決方法を調査中。

追記2 2011/11/30)
解決しました。次回記事にて詳細を記述します。

[ FLASH ] [ tips ] 投稿者 siratama : 2011年11月10日 13:39

トラックバック

http://www.dango-itimi.com/blog/mt-tb.cgi/1053

» 暗号化調査3 : Crypto.getCipher メソッドの利用 : DES, AES 修正版 from X-LABO
前回の記事「暗号化調査2 : as3crypto ←→ PyCrypto : D... [続きを読む]

トラックバック時刻: 2011年12月01日 11:00

コメント

以下コメントを書き込むだけでは、管理人には通知が行われません。通知を行いたい場合、管理人の書き込みに「返信」を押してコメントをしていただくか、あるいは Google+, Twitter へご連絡ください。




[EDIT]