Python 2与Java 1.6之间的AES 128 CBC BadPaddingException

2 投票
5 回答
4233 浏览
提问于 2025-04-16 14:02

我有一个用Python写的服务器脚本和一个用Java写的客户端应用,目标是通过网络连接加密数据。数据总是一个字符串。(我对Java还不太熟悉)

应该发生的事情:
Python脚本创建一个socket(网络连接),监听连接请求,接受新的连接,然后发送一个明文的初始化向量(IV),用于AES CBC PKCS5Padding加密,设置好加密方式后,从Java应用接收加密数据(解密后在终端打印出来),然后再发送加密数据给Java应用,最后关闭连接。

Java应用连接到Python的socket,接收IV,设置相同的加密方式,给一个简单的字符串加密后发送给Python服务器,然后等待回复,解密回复并在屏幕上打印出来。连接结束后关闭。

实际发生的事情:
服务器设置好socket,Java客户端连接并接收IV,双方都设置了相同的加密和解密方式,然后Java客户端发送一个加密的字符串。Python服务器成功接收到加密数据,解密后去掉填充,字符串显示正常。然后服务器发送一个加密的字符串给Java客户端,客户端接收到这个字符串,但在解密时出现了BadPaddingException错误。

我已经确认Python那边的PKCS5填充是正确的,符合RFC标准。我尝试了其他的填充方法(比如零填充等),但都没有成功。我还尝试了不同的字符串编码,也试过M2Crypto(现在用的是pycrypto)等等。我还尝试在客户端使用CipherInputStream来处理socket,但结果还是一样。

没有任何方法奏效。我仍然觉得可能是Python和Java之间的编码问题,但我现在不知道为什么Java客户端在解密时会失败。

Python服务器:

#!/usr/bin/env python

import os, time, threading, json
from socket import *
import sys, base64

from Crypto.Cipher import AES
from socket import *

### Settings
serverHost = '' # localhost
serverPort = 5555 # non-reserved
masterkey = 'mysecretpassword'

BLOCK_SIZE = 16 # Block-size for cipher (16, 24 or 32 for AES)
#PADDING = '{' # block padding for AES

# PKCS5 Padding
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s : s[0:-ord(s[-1])]

# generate new IV's - why you ask? ... just look at WEP
def createCipher(key):
    iv = os.urandom(16)
    return (AES.new(key, AES.MODE_CBC, iv), iv)

class IRCTalkServer(threading.Thread):
    def __init__(self, masterkey, host='', port=5555):
        try:
            self.sockobj = socket(AF_INET, SOCK_STREAM) # create TCP socket obj
            self.sockobj.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # make port reusable
            self.sockobj.bind((host, port)) # bind socket to port
            self.sockobj.listen(5)                     # listen, allow 5 pending connects
            if host == '': host='localhost'
            print "Started server on %s:%d" % (host, port)
        except:
            print "Error starting server"
            sys.exit(0)
        threading.Thread.__init__(self)
        self.die = False # loop killer
        self.masterkey = masterkey
        self.host = host
        self.port = port

    def run(self):
        try:
            while True and not self.die: # infinite loop unless called to quit
                connection, address = self.sockobj.accept()
                print 'Server connected by ', address
                # generate cipher and get first IV - prevent same WEP hacks
                #paddedKey = keypad(masterkey)
                #print "master key:", masterkey, " - padded key:", paddedKey, " - diff key:", keypad('test')
                self.cipher, self.iv = createCipher(masterkey)
                print "IV:", self.iv.encode('hex'), "Sending IV: ", repr(self.iv)
                connection.send("%s%s" % (self.iv.encode('hex'),'\n')) # send iv first
                while True and not self.die: # read from client
                    print "waiting for client"
                    data = connection.recv(10485760)
                    print "recieved from client:", repr(data.rstrip())
                    if not data:
                                                print "NO DATA"
                                                break
                    dataCheck, JSON = self.decryptData(data.rstrip())
                    print 'Recieved from Client:', repr(JSON)
                    #print 'Recieved from Client:', repr(data)
                    #if dataCheck:
                        #senddata = self.encryptData('SUCCESS')
                        #print "Size of compresseddata:", len(senddata)
                        #connection.send(senddata)
                        #morestuff = 'abc123def456'*int(10000/9)
                        #senddata = self.encryptData(['test', morestuff, 'test1', {'key': 'value'}, 2223])
                        #print "Size of compresseddata:", len(senddata)
                        #connection.send(senddata)
                    print "Sending reply to client..."
                    senddata = self.encryptData('test')
                    print "reply data:", repr("%s%s" % (senddata, "\n"))
                    connection.send("%s%s" % (senddata, "\n"))
                    #connection.send("Hello back mr android!")
                    #successReply = connection.recv(256) # only for "END REQUEST"
                                        break
                                print "Closing connection... \n\n"
                connection.close()
        except:
                        print "exception on try loop"
            pass # an error occurred, just drop it, the client will try again later

    def encryptData(self, plaintext):
        # convert to json string, pad the string, then encrypt, then compress
        #JSON = json.dumps(plaintext, separators=(',',':'))
        JSON = plaintext
        #print "Size of JSON:", len(JSON)
        ciphertext = pad(unicode(JSON))
        print "padded text:", repr(ciphertext)
        ciphertext = self.cipher.encrypt(ciphertext)
        print "ciphertext:", repr(ciphertext), "|", len(ciphertext), "|", ciphertext
        ciphertext = ciphertext.encode('hex').upper()
        print "hexified text:", repr(ciphertext)
        #ciphertext = self.cipher.encrypt(pad(JSON)).encode('hex').upper()
        print "Size of ciphertext:", len(ciphertext)
        return ciphertext

    def decryptData(self, ciphertext):
        try:
            # decompress data to ciphertext, decrypt, convert to json
            print "length of ciphertext:", len(ciphertext)
            ptext = ciphertext.decode('hex')
            print "unhexifed:", repr(ptext)
            ptext = self.cipher.decrypt(ptext)
            print "decrypted:", repr(ptext)
            ptext = unpad(ptext)
            print "unpadded:", repr(ptext)
            #ptext = unpad(self.cipher.decrypt(ciphertext.decode('hex')))
            print "ptext: ", repr(ptext)
            JSON = ptext
            #plaintext = unpad(self.cipher.decrypt(ciphertext))
            ##JSON = json.loads(plaintext)
            #JSON = plaintext
        except:
            print "Error on decryption"
            JSON = None
        return (True, JSON)

    def addToQueue(self, data):
        pass

    def getFromQueue(self):
        pass

testserver =IRCTalkServer(masterkey)
testserver.start()
testserver.join()

Java客户端:(这段代码的部分内容还需要适当注明来源 - [我还没做这件事])

import java.io.*;
import java.util.*;
import java.net.*;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
//import javax.crypto.CipherInputStream;
//import javax.crypto.CipherOutputStream;

import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.spec.SecretKeySpec;

class clientHandler {
    /*
     *  This class sets up AES CBC encryption for the socket, 
     *  new instance for every connection since the IV changes
     */

    // Declare variables
    private Cipher ecipher;
    private Cipher dcipher;
    Socket testSocket = null;
    //DataOutputStream out = null;
    //DataInputStream in = null;
    PrintWriter out = null;
    BufferedReader in = null;
    String masterkey = "mysecretpassword";
    static final String HEXES = "0123456789ABCDEF";

    public static String byteToHex( byte [] raw ) {
        if ( raw == null ) {
            return null;
        }
        final StringBuilder hex = new StringBuilder( 2 * raw.length );
        for ( final byte b : raw ) {
            hex.append(HEXES.charAt((b & 0xF0) >> 4))
                .append(HEXES.charAt((b & 0x0F)));
        }
        return hex.toString();
    }

    public static byte[] hexToByte(String hexString) {
        int len = hexString.length();
        byte[] ba = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            ba[i/2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i+1), 16));
        }
        return ba;
    }

    public String encrypt(String plaintext) {
        // encrypt string
        try {
            byte[] cipherbyte = ecipher.doFinal(plaintext.getBytes("UTF-8")); 
            //String ciphertext = new String(ecipher.doFinal(plaintext.getBytes("UTF8")));
            return byteToHex(cipherbyte);
        } catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    public String decrypt(String ciphertext) {
        // decrypt hex string
        try {
            System.out.println("decrypt byte length: " + hexToByte(ciphertext).length);
            String tp = new String(hexToByte(ciphertext), "UTF-8");
            System.out.println("toString(): " + tp);
            String plaintext = new String(dcipher.doFinal(hexToByte(ciphertext.trim())), "UTF-8");
            //String plaintext = new String(dcipher.doFinal(ciphertext.getBytes("UTF8")), "UTF-8");
            return plaintext;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public void setupCrypto(byte[] iv, String key) {
        // setup AES CBC encryption
        //  convert IV and key to byte array for crypto
        try {
             System.out.println("Setting up Crypto...");
             //byte[] ivb = iv.getBytes("UTF8");
             byte[] keyb = key.getBytes("UTF8");
             AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv);
             SecretKeySpec skey = new SecretKeySpec(keyb, "AES");
             ecipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
             dcipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
             ecipher.init(Cipher.ENCRYPT_MODE, skey, paramSpec);
             dcipher.init(Cipher.DECRYPT_MODE, skey, paramSpec);
        } catch (Exception e) {
            System.err.println("Error:" + e);
        }
    }

    public void startServer() {
        // starts server
        try {
            System.out.println("Connecting to Server");
            testSocket = new Socket("localhost", 5555);
            //out = new DataOutputStream(testSocket.getOutputStream());
            //in = new DataInputStream(testSocket.getInputStream());
            out = new PrintWriter(testSocket.getOutputStream(), true);
            in = new BufferedReader(new InputStreamReader(testSocket.getInputStream()));
        } catch (UnknownHostException e) {
            System.err.println("Uknown Host");
        } catch (IOException e) {
            System.err.println("Couldn't get I/O for the connection");
        }
    }

     public void androidClientHandler() {
         // actual communication
         if (testSocket != null && out != null && in != null) {
             try {
                 System.out.println("Waiting for IV..");
                 String iv;
                 iv = in.readLine();
                 byte [] ivb = hexToByte(iv);
                 System.out.println("Got IV..." + iv);
                 setupCrypto(ivb, masterkey);
                 String ciphertext;
                 String plaintext;
                 System.out.println("Sending \"test\" to server");
                 //out.writeBytes("test\n");
                 ciphertext = encrypt("test");
                 System.out.println("Sent: " + ciphertext);
                 out.println(ciphertext);
                 System.out.println("Waiting for Server to reply");
                 String responseLine;
                 responseLine = in.readLine().replaceAll("\\\\n", "");
                 System.out.println("Recieved from Server: " + responseLine + " - length: " + responseLine.length());
                 plaintext = decrypt(responseLine);
                 System.out.println("Recieved from Server: " + plaintext);
                 System.out.println("Closing Connection");
                 out.close();
                 in.close();
                 testSocket.close();
             } catch (UnknownHostException e) {
                 System.err.println("Trying to connect to unknown host:" + e);
             } catch (IOException e) {
                 System.err.println("IOException: " + e);
             }
         }
     }

     public void javaIsGay() {
         startServer();
         androidClientHandler();
     }

     public static void main(String[] args) {
         System.setProperty("file.encoding", "UTF-8");
         clientHandler c = new clientHandler();
         c.javaIsGay();
     }
}

Python服务器的输出:

Started server on localhost:5555
Server connected by  ('127.0.0.1', 59683)
IV: c54aae0a5c43f547f0355ee7a0ee38c1 Sending IV:  '\xc5J\xae\n\\C\xf5G\xf05^\xe7\xa0\xee8\xc1'
waiting for client
recieved from client: 'BBE7E09093625204CD3F7B755066419D'
length of ciphertext: 32
unhexifed: '\xbb\xe7\xe0\x90\x93bR\x04\xcd?{uPfA\x9d'
decrypted: 'test\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
unpadded: 'test'
ptext:  'test'
Recieved from Client: 'test'
Sending reply to client...
padded text: u'test\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
ciphertext: '\x9fT\xfc\xf0\xa8\xc2\xd3N*\x8f\x8e~\xc7\x8a\xbfR' | 16 | �T�����N*~NJ�R
hexified text: '9F54FCF0A8C2D34E2A8F8E7EC78ABF52'
Size of ciphertext: 32
reply data: '9F54FCF0A8C2D34E2A8F8E7EC78ABF52\n'
Closing connection... 

Java客户端的输出:

Connecting to Server
Waiting for IV..
Got IV...c54aae0a5c43f547f0355ee7a0ee38c1
Setting up Crypto...
Sending "test" to server
Sent: BBE7E09093625204CD3F7B755066419D
Waiting for Server to reply
Recieved from Server: 9F54FCF0A8C2D34E2A8F8E7EC78ABF52 - length: 32
decrypt byte length: 16
toString(): �T����N*��~NJ�R
javax.crypto.BadPaddingException: Given final block not properly padded
    at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:811)
    at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:676)
    at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:317)
    at javax.crypto.Cipher.doFinal(Cipher.java:1813)
    at clientHandler.decrypt(SocketClient.java:70)
    at clientHandler.androidClientHandler(SocketClient.java:134)
    at clientHandler.javaIsGay(SocketClient.java:151)
    at clientHandler.main(SocketClient.java:157)
Recieved from Server: null
Closing Connection

5 个回答

0

在你使用的Java程序中,进行加密和解密时用了两个不同的Cipher实例,它们都用相同的初始向量(IV)来初始化。而在你的Python代码中,你是用同一个对象来进行加密和解密。我对pycrypto这个库不太熟悉(而且对Python也不太精通),但很可能这个加密对象内部有一些状态,特别是在CBC模式下。如果你在初次解密后没有重置这个加密对象,那么我不确定你在加密回复时是否使用了预期的初始向量,这可能就是导致异常的原因。

2

我可能来得有点晚,但我有一些想法:
你在Python中的填充程序有个问题。如果明文的大小是16的倍数,就不会添加填充,而Java却期待有16个'\x10'。

当然,这可能并不能解决你在传输过程中出现的损坏问题。你最好的办法是检查一下问题是出在加密算法上还是传输上:不如把编码后的文本写到一个文件里,然后让Java从这个文件中读取,确保一下?

0

你提到过尝试不填充数据,那你有没有试过换一下加密模式?对于你的应用来说,CTR模式应该和CBC模式一样安全,而且不需要填充数据。

撰写回答