Java と Python 相互で AES 256 暗号/複合を実行する

本記事よりも、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で暗号→ PythonBase64エンコードして受信してJavaでの複合、
Python暗号→Java複合

String plane 
= new String(Base64.getDecoder().decode(decrypt(encedtxt)), StandardCharsets.UTF_8);

Java暗号→Java複合

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吹きプログラマの黙示録
以下は、それまでの、パディングが、JavaPython の差
をどう吸収させるか苦労した形跡。。。

問題の 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))