本記事よりも、2019-3-2 に書き改めた方を参照すべし。。
oboe2uran.hatenablog.com
試したのは、CBCモードとECBモードです。
Python暗号→Java複合は問題ないのですが、Base64 エンコードで相互受け渡すとして
Java暗号→Python複合では、暗号化した文字列長さが4の倍数になっても、
暗号文を Base64 b64decode 実行した時に、
binascii.Error: Incorrect padding
が発生してしまいます。
解決した方法は、Java暗号→Python複合の受け渡しを Base64 エンコードではなくて
別の表現、16進数 HEX表現文字列を渡すことにしました。
Java 側の準備
クラス内で、private 変数で以下を用意します。
private SecretKeySpec key; private IvParameterSpec iv; private byte[] ivary; private String mode;
コンストラクタなどで、共通鍵になる String password と
イニシャライズベクトル値 byte[] ivector を受け取って準備します。
以下は、CBCモードです。ECBモードなら、ベクトル値は不要
try{ byte[] keydata = password.getBytes(); MessageDigest sha = MessageDigest.getInstance("SHA-256"); keydata = sha.digest(keydata); keydata = Arrays.copyOf(keydata, 32); key = new SecretKeySpec(keydata, "AES"); mode = "AES/CBC/PKCS5Padding"; ivary = Arrays.copyOf(ivector, 16); iv = new IvParameterSpec(ivary); }catch(NoSuchAlgorithmException e){ throw new RuntimeException(e.getMessage()); }
CBCモード 暗号化
public byte[] encrypt(String message){ try{ Cipher cipher = Cipher.getInstance(mode); cipher.init(Cipher.ENCRYPT_MODE, key, iv); return cipher.doFinal(message.getBytes()); }catch(Exception e){ throw new RuntimeException(e); } }
CBCモード 複合化
public byte[] decrypt(String encryptText){ try{ Cipher cipher = Cipher.getInstance(mode); cipher.init(Cipher.DECRYPT_MODE, key, iv); byte[] cipherBytes = Base64.getDecoder().decode(encryptText); return cipher.doFinal(cipherBytes); }catch(Exception e){ throw new RuntimeException(e); } }
ベクトル値の決定は状況によっていろんな方法あると思いますが、
例えば、16進数 HEX表現文字列から求めるなら、
List<Byte> listb = Pattern.compile("[\\w]{1,2}").matcher(ivhex) .results().map(r->(byte)Integer.parseInt(r.group(), 16)) .collect(Collectors.toList()); byte[] iv = Bytes.toArray(listb);
でも良いと思います。
Pythonで暗号→ Python で Base64エンコードして受信してJavaでの複合、
Python暗号→Java複合
String plane
= new String(Base64.getDecoder().decode(decrypt(encedtxt)), StandardCharsets.UTF_8);
String plane
= new String(decode(decrypt(encedtxt)), StandardCharsets.UTF_8);
この差があります。
Java暗号→Python複合では、
Javaで暗号、Base64エンコードして渡したいのですが、それだと
Python側で Base64 b64decode 処理でエラーになるので、
以下のように、暗号化したら、16進数 HEX表現文字列にして渡すことにします。
byte[] encdat = cipher.encrypt(planetxt); String hexdata = IntStream.range(0, encdat.length) .mapToObj(i->String.format("%02x", encdat[i])) .collect(Collectors.joining(""));
Python 側
Pythonソース名、cryptoaes256.py とか、名付けてます。
暗号メソッド、複合メソッド、CBCモード、ECBモードごとに分けるのもどうかと
思ったのですが、コンストラクタ生成で決めるのも引数多くて嫌だし、
メソッドに引数でモード指定するのも嫌だし
かっこ悪くてもメソッド名で区別することにしました。
→ 考え直し、書き直す!!
他言語と相互間の暗合複合を考慮の AES 暗合複合 Python のコード - Oboe吹きプログラマの黙示録
以下は、それまでの、パディングが、Java と Python の差
をどう吸収させるか苦労した形跡。。。
問題の Java で暗号化したものを 16進 Hex表現で受け取った時の複合だけ
decryptCBCbin と decryptECBbin というメソッドを呼び出します。
# -*- coding: UTF-8 -*- import re import hashlib from Crypto.Cipher import AES from Crypto import Random from base64 import b64encode, b64decode BLOCK_SIZE = AES.block_size pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) #unpad = lambda s: s[:-ord(s[len(s) - 1:])] # 不使用 class AESCipher: def __init__(self, password, iv=Random.new().read(AES.block_size)): self.key = hashlib.sha256(password.encode("utf-8")).digest() self.iv = iv # ベクトル値取得 def getIV(self): return self.iv # password & ベクトル値セット def setPasswdAndIV(self, passandiv): self.key = hashlib.sha256(passandiv[0].encode("utf-8")).digest() self.iv = passandiv[1] # AES 256キー参照 def getKey(self): return self.key # CBC モード暗号化 plane → byte def encryptCBC(self, message): if message is None or len(message) == 0: raise NameError("No value given to encrypt") raw = b64encode(message.encode('utf-8')).decode() raw = pad(raw) raw = raw.encode('utf-8') cipher = AES.new(self.key, AES.MODE_CBC, self.iv) return cipher.encrypt(raw) # ECB モード暗号化 plane → byte def encryptECB(self, message): if message is None or len(message) == 0: raise NameError("No value given to encrypt") raw = b64encode(message.encode('utf-8')).decode() raw = pad(raw) raw = raw.encode('utf-8') cipher = AES.new(self.key, AES.MODE_ECB) return cipher.encrypt(raw) # CBC モード複合 encrypted base64 → plane def decryptCBC(self, enctext): if enctext is None or len(enctext) == 0: raise NameError("No value given to decrypt") cipher = AES.new(self.key, AES.MODE_CBC, self.iv) return b64decode(cipher.decrypt(b64decode(enctext))).decode() # CBC モード複合 encrypted HEX express binary → plane def decryptCBCbin(self, encbinary): if encbinary is None or len(encbinary) == 0: raise NameError("No value given to decrypt") cipher = AES.new(self.key, AES.MODE_CBC, self.iv) return re.sub(b'\x08*$', b'', cipher.decrypt(encbinary)).decode() # ECB モード複合 encrypted base64 → plane def decryptECB(self, enctxt): if enctxt is None or len(enctxt) == 0: raise NameError("No value given to decrypt") cipher = AES.new(self.key, AES.MODE_ECB) return b64decode(cipher.decrypt(b64decode(enctxt)).decode()).decode() # ECB モード複合 encrypted HEX express binary → plane def decryptECBbin(self, encbinary): if encbinary is None or len(encbinary) == 0: raise NameError("No value given to decrypt") cipher = AES.new(self.key, AES.MODE_ECB) return re.sub(b'\x08*$', b'', cipher.decrypt(encbinary)).decode() # password と iv を再セットするための タプルを生成 def createPasswordAndIV(): import random import string return (''.join(random.choices(string.ascii_letters + string.digits, k=16)), Random.new().read(AES.block_size) )
decryptCBCbin と decryptECBbin
return re.sub(b'\x08*$', b'', cipher.decrypt(encbinary)).decode()
ではなくて、最後の \x00 を置換するように
return re.sub(b'\x00*$', b'', cipher.decrypt(encbinary)).decode()
ではないかとも思ったのですが、実行してみると、 \x08 にしないとダメでした。
Python での暗号
from cryptoaes256 import AESCipher from base64 import b64encode # 鍵や vector は適当に持ってくる aes = AESCipher(key, bytes.fromhex(ivhex)) enctxt = b64encode(aes.encryptCBC(planetxt)).decode('utf-8')
Python で暗号したものを複合
aes = AESCipher(key, bytes.fromhex(ivhex))
planetxt = aes.decryptCBC(enctext)
Javaで暗号化→16進 Hex文字列を受け取って、複合
from cryptoaes256 import AESCipher aes = AESCipher(key, bytes.fromhex(ivhex)) planetxt = aes.decryptCBCbin(bytes.fromhex(enchexstring))