Erro: javax.crypto.BadPaddingException: Data must start with zero

Ao executar o processo de descriptografia de um um array de bytes de um String já criptografado me deparo com o seguinte erro:


javax.crypto.BadPaddingException: Data must start with zero
	at sun.security.rsa.RSAPadding.unpadV15(Unknown Source)
	at sun.security.rsa.RSAPadding.unpad(Unknown Source)
	at com.sun.crypto.provider.RSACipher.a(DashoA13*..)
	at com.sun.crypto.provider.RSACipher.engineDoFinal(DashoA13*..)
	at javax.crypto.Cipher.doFinal(DashoA13*..)
	at DecryptMsg.decrypt(DecryptMsg.java:19)
	at GrafClient$Reader.run(GrafClient.java:281)

O código da classe Decrypt está logo abaixo (Obs.: Fiz um comentário na linha onde está o erro)


import java.security.Key;
import javax.crypto.Cipher;
import java.security.Security;

public class DecryptMsg {
	
	 /* Construtor da classe */ 
	
	  public DecryptMsg() {
		  
	  }
	 
      public byte[] decrypt(Key privk, byte[] cipherText) throws Exception { 	      
       	    	    	
        Cipher c;	    	            
	    	
	    c = Cipher.getInstance("RSA");	        
    	c.init(Cipher.DECRYPT_MODE, privk);	    	
    	byte[] plainText = c.doFinal(cipherText); // linha que gera o erro
    	System.out.println("plaintext: " + new String (plainText));    				
		
		return plainText;
		 
	}

}

Já pesquisei em outros Fóruns mas nada. Utilizo sockets e recebo do servidor a mensagem criptografada e o cliente faz o processo inverso.

Abaixo tem o exemplo em uma única classe que funcionou perfeitamente e faz o processo de captura da chave privada/pública da mesma forma que o programa.


import java.io.FileInputStream;
import java.io.InputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import javax.crypto.Cipher;

public class RSAexample {

  public static void main(String[] args) throws Exception {    
    	
     byte[] input = "Raphael".getBytes();
     String password = "123456", alias = "usuario1";
	
      /* begin public key */
	
      FileInputStream fis = new FileInputStream("src/pub_usuario1.cert");
      CertificateFactory certF = CertificateFactory.getInstance("X.509");
      X509Certificate cert = (X509Certificate)certF.generateCertificate(fis);   
      PublicKey pubKey = cert.getPublicKey();
      fis.close();
    
      /* end public key */  
    
	
      /* begin private key */
    
      KeyStore ks = KeyStore.getInstance ("JKS");
      char[] pwd = password.toCharArray();
      InputStream is = new FileInputStream("src/usuario1.jks");
      ks.load(is, pwd);
      is.close();
      Key privKey = ks.getKey(alias, pwd);    
      is.close();
   
      /* end private key */

      /* encrypt */
    
      Cipher cipher = Cipher.getInstance("RSA");
      cipher.init(Cipher.ENCRYPT_MODE, pubKey);
      byte[] cipherText = cipher.doFinal(input);
      System.out.println("cipher: " + new String(cipherText));

      /* decrypt */
    
      Cipher cipher2 = Cipher.getInstance("RSA");
      cipher2.init(Cipher.DECRYPT_MODE, privKey);
      byte[] plainText = cipher2.doFinal(cipherText);
      System.out.println("plain : " + new String(plainText));

   }
}

Por favor me ajudem nesta questão.

Grato desde já pela oportunidade.

Nunca, nunca, nunca, nunca mesmo, converta um array de bytes para uma string usando “new String”, a menos que esse array de bytes tenha vindo da operação “getBytes” de uma outra string, e com a mesma codificação. É isso, e só isso, que está provocando seus problemas.

Converta o array de bytes para hexadecimal ou para Base-64 (recomendado) se precisar trafegá-lo (ou armazená-lo) como string.

Realizei as conversões usando BASE-64 mas a mensagem ainda aparece:

Abaixo alguns trechos de código do processo de comunicação servidor - cliente

  1. Envio do cliente para o servidor:

    String line;
    byte[] cipherTxt =null;
    cipherTxt = encryptmsg.encrypt(pubk, line.getBytes());
    out.println(e64.encode(cipherTxt));

  2. Servidor recebe do cliente:

    String line;
    inline = in.readLine();

  3. Servidor envia para cliente:

    out.println(b64.decodeBuffer(outputline));

  4. Cliente recebe do servidor:

    String line;
    byte[] result = null;
    line = in.readLine();

    result = decryptmsg.decrypt(privk, b64.decodeBuffer(line));
    System.out(“result: " + e64.encodeBuffer(result) +”\r\n");

Basicamente esse é o processo entre servidor-cliente usando socket e criptografia assimétrica (chave pública/privada).

Obs.: A instância do algoritmo para o objeto Cipher está da seguinte forma:

c = Cipher.getInstance(“RSA”);

Por favor, isso resolvendo já me ajuda muito.

Grato.

Dica 1: tire os sockets da jogada e veja se você está conseguindo decifrar a mensagem que foi cifrada. Se isso estiver funcionando corretamente, o problema é com os seus sockets.

Dica 2: nunca use criptografia assimétrica pura para criptografar dados. Ela deve ser usada apenas para:
a) Criptografar chaves simétricas (você criptografa um bloco de 8 ou 16 bytes apenas, que é uma chave simétrica);
b) Fazer assinaturas digitais (você criptografa um hash de 16, 20 ou 32 bytes).

Já realizei a DICA 1, conforme você denominou, e funcionou perfeitamente. Objeto criptografado pela chave pública e objeto descriptografado pela chave privada. O exemplo está no início deste mesmo tópico.
Quero utilizar a criptografia com desejo de mostra a propriedade SIGILO no tráfego de informações pela rede. Por isso estou optando por chave pública -> chave privada. Ah… também desejo identificar um retardo por motivo deste processo e assim avaliar fluxo de transmissão com e sem criptografia. Largar isso agora não estaria de acordo com o objetivo da minha aplicação.
Se houver mais alguma comentário ou explicação que me ajude eu agradeço. As dicas tem sido muito valiosas e tem me ajudado a entender melhor o que não devo fazer. Caso seja possível, me ajude na questão do envio pra o servidor e do servidor para o cliente.

Por favor!

Raphael

Criptografia RSA é extremamente lenta para criptografar dados; realmente, se você usar a criptografia RSA exclusivamente, vai ver que você terá um atraso significativo na transmissão dos dados.
.
Além disso, se você criptografar muitos dados com a mesma chave RSA, ela pode ser mais facilmente quebrada (não consigo me lembrar mais onde li isso, mas era em um paper de algum criptógrafo famoso), portanto não se recomenda efetuar criptografia dos dados com RSA, apenas das chaves simétricas.

É por isso que o SSL (que é o que você deveria ter usado desde o começo, a menos que seja um projeto de escola) usa criptografia RSA apenas para efetuar a parte de transmissão da chave de sessão e verificação de certificados; no resto da conversação, é usado um algoritmo simétrico como o AES-256 (como o Firefox ou o IE 8 fazem) ou o Triple-DES (como o IE 7 ou 6 fazem) ou o RC4 (como o IE 5 ou anteriores fazem). Todos esses algoritmos são suportados pelo Java (até 128 bits em uma configuração padrão, até 256 bits se você usar alguns arquivos que estão disponíveis no site da Sun.)

Abandone o protocolo de sockets baseado em texto (usando, por exemplo, BufferedReader x BufferedWriter).
Eles são mais difíceis de funcionar direito. Mesmo o http, que é um protocolo baseado em texto, tem seus problemas (tanto é que, para enviar dados binários, como imagens, é imprescindível que o header “Content-Length” esteja corretamente setado para que o http funcione direito.)

Em vez disso, encapsule o socket em um DataInputStream / DataOutputStream, e mande seus dados usando um protocolo bem simples:

  • 2 primeiros bytes - Tamanho da mensagem a ser transmitida (use os métodos writeShort / readShort);
  • Os bytes da mensagem - A mensagem a ser transmitida/recebida

Note que, ao receber a mensagem, pode ser que não cheguem todos os bytes de uma vez. Portanto, já que você recebeu o tamanho da mensagem em 2 bytes, continue acumulando os dados recebidos até que cheguem todos os bytes, ou então que o método read() retorne -1, indicando que o socket foi fechado por algum motivo.

import java.net.*;
import java.io.*;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;

// Este programa é um servidor simples, que recebe mensagens simples e as devolve cifradas.
// O cliente, por sua vez, manda mensagens simples, recebe as mensagens cifradas, e tenta decifrá-las.
// Só para não ficar muito complicado, vou usar uma cifra bem simples.
// Para ficar mais simples ainda, vamos usar uma thread para cada socket, 
// o que normalmente não é recomendado.

class SimpleServer {
    public class ServerConnection implements Runnable {
        public ServerConnection (Socket socket) {
	    this.socket = socket;
	}
	public void run() {
	    try { socket.setTcpNoDelay (true); } catch (SocketException ex) {}
	    // Deste socket, devemos tentar ler uma mensagem, e depois devolvê-la.
	    // Enquanto houver mensagens, iremos cifrá-las, até não conseguirmos mais.
	    // Além disso, vamos supor que nunca venham mais de 32767 bytes. 
	    // Se o valor lido for mais que isso, então desistiremos
	    // porque "entrou boi na linha". 
	    DataInputStream dis = null; 
	    DataOutputStream dos = null;
	    try {
	        dis = new DataInputStream (socket.getInputStream());	    
	        dos = new DataOutputStream (socket.getOutputStream());
	    exit:
		while (true) {
		    ByteArrayOutputStream baos = new ByteArrayOutputStream();
		    int length = dis.readShort ();
		    System.out.println ("Devem ser lidos " + length);
		    int bytesLidos = 0;
		    byte[] buffer = new byte [1024];
		    while (bytesLidos < length) {
			int nBytes = dis.read (buffer, 0, Math.min (length - bytesLidos, buffer.length));
			System.out.println ("Lidos " + nBytes);
			if (nBytes < 0) {
			    break exit;
			}
			bytesLidos += nBytes;
			baos.write (buffer, 0, nBytes);
		    }
		    byte[] dadosEmClaro = baos.toByteArray();
		    System.out.println ("Recebidos " + dadosEmClaro.length);
		    // Vamos agora criptografar os dados...
		    byte[] dadosCifrados = cipher.doFinal (dadosEmClaro);
		    dos.writeShort (dadosCifrados.length);
		    dos.write (dadosCifrados, 0, dadosCifrados.length);
		}
	    } catch (IOException ex) {
	        ex.printStackTrace();
	    } catch (Exception ex) {
	        ex.printStackTrace();
	    } finally {
	        try { if (dis != null) dis.close(); } catch (IOException ex) { ex.printStackTrace(); }
	        try { if (dos != null) dos.close(); } catch (IOException ex) { ex.printStackTrace(); }
		try { socket.close(); } catch (IOException ex) { ex.printStackTrace(); }
		System.out.println ("Conexão encerrada com " + socket.toString());
	    }
	}
	private Socket socket;
    }

    public SimpleServer(int port, Cipher cipher) {
        this.port = port; this.cipher = cipher;
    }
    public void go() throws IOException {
        System.out.println ("SimpleServer listening to port " + port + "...");
	serverSocket = new ServerSocket (port);
	for (Socket socket = serverSocket.accept(); socket != null; socket = serverSocket.accept()) {
	    System.out.println ("Accepted connection from " + socket.toString());
	    new Thread (new ServerConnection(socket)).start();
	    try {Thread.sleep (1000);} catch (InterruptedException ex) {}
	}
    }
    private int port;
    private Cipher cipher;
    private ServerSocket serverSocket;
    public static void main(String[] args) throws Exception {
	int port = 12345;
	Cipher aescf = Cipher.getInstance ("AES/CBC/PKCS5Padding");
	IvParameterSpec ivspec = new IvParameterSpec (new byte[16]);
	byte[] chave = "PasswordPassword".getBytes();
	aescf.init (Cipher.ENCRYPT_MODE, new SecretKeySpec (chave, "AES"), ivspec);
        SimpleServer ss = new SimpleServer(port, aescf);
	ss.go();
    }
}

class SimpleClient {
    public SimpleClient(int port, InetAddress addr, Cipher cipher) {
        this.port = port;
	this.cipher = cipher;
	this.addr = addr;
    }
    private int port;
    private Cipher cipher;
    private InetAddress addr;

    public void go () throws IOException, SecurityException {
        Socket sock = new Socket (addr, port);
	sock.setTcpNoDelay (true);
	DataInputStream dis = null;
	DataOutputStream dos = null;
	try {
	    dis = new DataInputStream (sock.getInputStream ());
	    dos = new DataOutputStream (sock.getOutputStream());
	    int n = 0;
	    exit:
		while (true) {
		    byte[] dadosEmClaro = Integer.toString (n++).getBytes();
		    System.out.println ("Enviando " + dadosEmClaro.length);
		    dos.writeShort (dadosEmClaro.length);
		    dos.write (dadosEmClaro, 0, dadosEmClaro.length);
		    ByteArrayOutputStream baos = new ByteArrayOutputStream();
		    int length = dis.readShort ();
		    System.out.println ("Devem ser recebidos " + length);
		    int bytesLidos = 0;
		    byte[] buffer = new byte [1024];
		    while (bytesLidos < length) {
			int nBytes = dis.read (buffer, 0, Math.min (length - bytesLidos, buffer.length));
			System.out.println ("Lidos " + nBytes);
			if (nBytes < 0) {
			    break exit;
			}
			bytesLidos += nBytes;
			baos.write (buffer, 0, nBytes);
		    }
		    byte[] dadosCriptografados = baos.toByteArray();
		    System.out.println ("Recebidos " + dadosCriptografados.length);
		    byte[] dadosDecifrados = cipher.doFinal (dadosCriptografados);
		    System.out.println (Arrays.equals (dadosEmClaro, dadosDecifrados));
		}
	} catch (Exception ex) {
	    ex.printStackTrace();
	} finally {
	    if (dis != null) try { dis.close(); } catch (IOException ex) { ex.printStackTrace(); }
	    if (dos != null) try { dos.close(); } catch (IOException ex) { ex.printStackTrace(); }
	}
    }
    
    public static void main(String[] args) throws Exception {
        int port = 12345;
	InetAddress addr = InetAddress.getByName ("localhost");
	Cipher aescf = Cipher.getInstance ("AES/CBC/PKCS5Padding");
	IvParameterSpec ivspec = new IvParameterSpec (new byte[16]);
	byte[] chave = "PasswordPassword".getBytes();
	aescf.init (Cipher.DECRYPT_MODE, new SecretKeySpec (chave, "AES"), ivspec);
	SimpleClient sc = new SimpleClient (port, addr, aescf);
	sc.go();
    }
}

Note que o programa acima é um exemplo claro do que não deve ser feito - programação “macarrônica”. Só faltava ter um “goto”.