0

I have a problem decrypting; I'm getting the following error:

javax.crypto.BadPaddingException: Decryption error

My code to encrypt is as follows:

userName = URLDecoder.decode(userName, "ISO-8859-1");
Cipher objCipherTunkicloud = Cipher.getInstance("RSA/ECB/PKCS1Padding");

objCipherTunkicloud.init(Cipher.ENCRYPT_MODE, loadPublicKey("/keylecordonbleu/public.key", "RSA"));

byte[] arrDecryptedKeyBytes = objCipherTunkicloud.doFinal(userName.getBytes(StandardCharsets.UTF_8));
log.error("SECURITY - key en array de bytes");

String tkn = new String(arrDecryptedKeyBytes);

userName = URLEncoder.encode(tkn, "ISO-8859-1");

and to decrypt it is this:

userName = URLDecoder.decode(userName, "ISO-8859-1");

Cipher objCipherTunkicloud = Cipher.getInstance("RSA/ECB/PKCS1Padding");

objCipherTunkicloud.init(Cipher.DECRYPT_MODE, loadPrivateKey("/keylecordonbleu/private.key", "RSA"));

byte[] arrDecryptedKeyBytes = objCipherTunkicloud.doFinal(userName.getBytes(StandardCharsets.ISO_8859_1));

String tkn = new String(arrDecryptedKeyBytes);

What is the problem, and how can I fix it?

the error comes to me in this line when decrypting:

byte[] arrDecryptedKeyBytes = objCipherTunkicloud.doFinal(userName.getBytes(StandardCharse‌​ts.ISO_8859_1));
  • 4
    `new String(arrDecryptedKeyBytes)` makes no sense. Encryption generates binary data, i.e. random bytes that do not represent characters in your platform default encoding. Don't transform these bytes to a String, or do it by base64-encoding the bytes. – JB Nizet Sep 15 '17 at 17:13
  • the error comes to me in this line when decrypting: byte[] arrDecryptedKeyBytes = objCipherTunkicloud.doFinal(userName.getBytes(StandardCharsets.ISO_8859_1)); – David Pineda Canales Sep 15 '17 at 17:18
  • In general a bad padding error means the decryption failed usually due to an incorrect key or data, the error may be an incorrect encoding. – zaph Sep 15 '17 at 18:26
  • @JBNizet: converting with the default charset is unsafe, but converting as 8859-1 is safe IF you subsequently preserve all 8859-1 chars, which this code does (albeit oddly) – dave_thompson_085 Sep 16 '17 at 21:27
  • @dave_thompson_085: `String tkn = new String(arrDecryptedKeyBytes);`. That's not using ISO-8859-1 (or it does by accident). – JB Nizet Sep 16 '17 at 21:30
  • @JBNizet: that why I said default is unsafe -- but _specifying_ 8859-1 with URL but _without_ base64 is safe; see my (now) answer. – dave_thompson_085 Sep 16 '17 at 21:41

3 Answers3

2

The comment by @JB Nizet explains and solves your problem. I'll elaborate it a bit:

Theory

  • The result of a (good) encryption is random, e.g. an array of random bytes. Therefore, the result will contain all possible byte combinations.
  • String(byte[]) interprets the given byte array as character data in the default (or the given) encoding.
  • Not all bytes or byte sequences represent (depending on the encoding) valid characters. The behavior on invalid bytes / sequences is undefined - they might just be ignored.
  • Therefore, String(byte[] encrypted).getBytes() does not return encrypted for all possible byte arrays.
  • Your code will therefore fail for some input.
  • Base64 (java.util.Base64) is usually used to print the result of an encryption.
  • Some crypto algorithms only work on blocks of a certain length. Cipher takes care of this and pads your input as necessary.
  • If your encoding/decoding cycle drops characters, the bytes to decode my no longer align to the needed block size and you get a BadPaddingException

Using ISO-8859-1 / Latin1

As @dave_thompson_085 points out, you can convert the byte array to a String and back without loss if you force java to interpret the byte array as ISO-8859-1 (latin1) using String(encrypted, StandardCharsets.ISO_8859_1) and encrypted.getBytes(StandardCharsets.ISO_8859_1). ISO-8859-1 maps all 256 byte values and does not have any "invalid values". I added a respective encoder/decoder to my code.

BUT: Make sure you know the consequences when going this route:

  • A byte array is just not a String! Abusing types has many side effects.
  • As soon as you try to read this String in another Program, you have to be sure your target system (and everything under way) has the same idea on how to treat Latin1. 0x00could mark the end of a string, 0x0a and 0x0d could be manipulated, control characters interpreted.
  • There's a reason why Base64 is usually used for encrypted texts...

Code

Your code does quite a bit of different things. Especially with cryptography, it usually pays to separate concerns and test them independently. To reproduce and solve the problem, I made some changes:

  • I left out the URLDecode / URLEncode as it does not contribute to the problem (and might belong into another layer...).
  • We don't have access to your loadPublicKey("/keylecordonbleu/public.key", "RSA") method and your key file... I replaced this by a KeyPair I generate on every test. You may want to start with this and add your keys once the other code is working.
  • I extracted the code to encode the encrypted byte[] to a String and to decode the String to a byte[] to be decrypted.

This allows you:

  • to test the bare en-/decrypt cycle encryptDecryptRoundtrip(String userName) without encoding to a String. This already worked in your code.
  • to test your byte[] -> String -> byte[] encoding (testSimpleEncoder()) that obviously doesn't work for all input and verify the same works with a Base64 encoding (testBase64Encoder()).

Class with your de-/encrypt code:

public class CryptoUtils {
  private static final String ALGORITHM = "RSA";
  private static final String TRANSFORMATION = ALGORITHM + "/ECB/PKCS1Padding";

  public interface Encoder extends Function<byte[], String> { };
  public interface Decoder extends Function<String, byte[]> { };

  public static class EncoderNotWorking implements Encoder {
    @Override
    public String apply(byte[] encrypted) {
      return new String(encrypted);
    }
  }

  public static class DecoderNotWorking implements Decoder {
    @Override
    public byte[] apply(String encrypted) {
      return encrypted.getBytes();
    }
  }

  public static class EncoderLatin1 implements Encoder {
    @Override
    public String apply(byte[] encrypted) {
      return new String(encrypted, StandardCharsets.ISO_8859_1);
    }
  }

  public static class DecoderLatin1 implements Decoder {
    @Override
    public byte[] apply(String encrypted) {
      return encrypted.getBytes(StandardCharsets.ISO_8859_1);
    }
  }

  public static class EncoderBase64 implements Encoder {
    @Override
    public String apply(byte[] encrypted) {
      return new String(Base64.getEncoder().encode(encrypted));
    }
  }

  public static class DecoderBase64 implements Decoder {
    @Override
    public byte[] apply(String encrypted) {
      return Base64.getDecoder().decode(encrypted);
    }
  }

  /** Return Cipher for the given mode (de/encrypt) and key. */
  public Cipher getInitCipher(int opmode, Key key) throws InvalidKeyException,
      NoSuchAlgorithmException, NoSuchPaddingException {
    Cipher cipher = Cipher.getInstance(TRANSFORMATION);
    cipher.init(opmode, key);
    return cipher;
  }

  /** Generate a key pair for testing. */
  public KeyPair generateKeyPair()
      throws NoSuchAlgorithmException, NoSuchProviderException {
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
    SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
    keyGen.initialize(1024, random);
    return keyGen.generateKeyPair();
  }

  public byte[] encrypt(Key publicKey, String userName)
      throws InvalidKeyException, NoSuchAlgorithmException,
      NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
    byte[] toEncrypt = userName.getBytes();
    Cipher cipher = getInitCipher(Cipher.ENCRYPT_MODE, publicKey);
    return cipher.doFinal(toEncrypt);
  }

  public String decrypt(Key privateKey, byte[] encryptedUserName)
      throws InvalidKeyException, NoSuchAlgorithmException,
      NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
    Cipher cipher = getInitCipher(Cipher.DECRYPT_MODE, privateKey);
    byte[] decrypted = cipher.doFinal(encryptedUserName);
    return new String(decrypted);
  }

  /** Encrypt and encode using the given Encoder, */
  public String encryptAndEncode(Key publicKey, String userName,
      Encoder encoder) throws InvalidKeyException, NoSuchAlgorithmException,
      NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
    byte[] encrypted = encrypt(publicKey, userName);
    return encoder.apply(encrypted);
  }

  /** Decrypt and Decode using the given Decoder, */
  public String decodeAndDecrypt(Key privateKey, String encryptedUserName,
      Decoder decoder) throws InvalidKeyException, NoSuchAlgorithmException,
      NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
    byte[] toDecrypt = decoder.apply(encryptedUserName);
    return decrypt(privateKey, toDecrypt);
  }

  /** Encrypt and decrypt the given String, and assert the result is correct. */
  public void encryptDecryptRoundtrip(String userName)
      throws InvalidKeyException, NoSuchAlgorithmException,
      NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException,
      NoSuchProviderException {
    CryptoUtils crypto = new CryptoUtils();
    KeyPair keys = crypto.generateKeyPair();
    byte[] encrypted = crypto.encrypt(keys.getPublic(), userName);
    String decrypted = crypto.decrypt(keys.getPrivate(), encrypted);
    assert decrypted.equals(userName);
  }

  /**
   * As @link {@link #encryptDecryptRoundtrip(String)}, but further encodes and
   * decodes the result of the encryption to/from a String using the given
   * Encoder/Decoder before decrypting it.
   */
  public void encodeDecodeRoundtrip(Encoder encoder, Decoder decoder,
      String userName) throws InvalidKeyException, NoSuchAlgorithmException,
      NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException,
      NoSuchProviderException {
    CryptoUtils crypto = new CryptoUtils();
    KeyPair keys = crypto.generateKeyPair();
    String encrypted = crypto.encryptAndEncode(keys.getPublic(), userName,
        encoder);
    // encrypted could now be stored and later loaded...
    String decrypted = crypto.decodeAndDecrypt(keys.getPrivate(), encrypted,
        decoder);
    assert decrypted.equals(userName);
  }

  /** Test the working examples*/
  public static void main(String[] args) throws NoSuchAlgorithmException,
      NoSuchProviderException, InvalidKeyException, NoSuchPaddingException,
      IllegalBlockSizeException, BadPaddingException {
    CryptoUtils crypto = new CryptoUtils();
    String userName = "John Doe";
    crypto.encryptDecryptRoundtrip(userName);
    crypto.encodeDecodeRoundtrip(new EncoderBase64(), new DecoderBase64(),
        userName);
  }
}

test class:

import static org.junit.Assert.assertArrayEquals;
import org.junit.Test;

public class CryptoUtilsTest {

  /**
   * Byte array to test encoding.
   * 
   * @see https://stackoverflow.com/questions/1301402/example-invalid-utf8-string
   */
  private static final byte[] ENCODE_TEST_ARRAY = new byte[] { (byte) 0x00,
    (byte) 0x00, (byte) 0x0a, (byte) 0x0c, (byte) 0x0d, (byte) 0xc3,
    (byte) 0x28, (byte) 0x7f, (byte) 0x80, (byte) 0xfe, (byte) 0xff };

  public void encoderDecoderTest(Encoder encoder, Decoder decoder) {
    String encoded = encoder.apply(ENCODE_TEST_ARRAY);
    byte[] decoded = decoder.apply(encoded);
    assertArrayEquals("encoder \"" + encoder.getClass() + "\" / decoder \""
        + decoder.getClass() + "\" failed!", ENCODE_TEST_ARRAY, decoded);
  }

  /**
   * Shows that String(byte[] encrypted).getBytes() does not return encrypted
   * for all input, as some byte sequences can't be interpreted as a string as
   * there are bytes/sequences that just don't represent characters!           
   */
  @Test
  public void testSimpleEncoder() {
    Encoder encoder = new CryptoUtils.EncoderNotWorking();
    Decoder decoder = new CryptoUtils.DecoderNotWorking();
    encoderDecoderTest(encoder, decoder);
  }

  /**
   * Shows that encoding a byte array into a String interpreting it as Latin1
   * should work.
   */
  @Test
  public void testLatin1Encoder() {
    Encoder encoder = new CryptoUtils.EncoderLatin1();
    Decoder decoder = new CryptoUtils.DecoderLatin1();
    encoderDecoderTest(encoder, decoder);
  }

  /** Shows that Base64 encoder should be used to encode random byte arrays. */
  @Test
  public void testBase64Encoder() {
    Encoder encoder = new CryptoUtils.EncoderBase64();
    Decoder decoder = new CryptoUtils.DecoderBase64();
    encoderDecoderTest(encoder, decoder);
  }
}
sruetti
  • 532
  • 2
  • 7
1

It looks like the problem is that you are encrypting with doFinal(userName.getBytes(StandardCharsets.UTF_8)); and decrypting with doFinal(userName.getBytes(StandardCharsets.ISO_8859_1));

geneSummons
  • 907
  • 5
  • 15
0

Your use of URL encoding for ciphertext is unusual, inefficient, and in general might be unsafe, but URL encoding as implemented by Java can preserve all 256 chars of 8859-1 if you create them correctly. The following compressed extract of your code does work if the fourth argument is supplied causing the new String(encryptedbytes) to specify 8859-1 instead of defaulting to the JVM's default charset, which varies by platform and environment and often is not 8859-1.

static void SO46244541CryptAsURL (String... args) throws Exception {
    // arguments: data pubkeyfile(der) prvkeyfile(der) flag(if present specify 8859-1 on conversion)
    String clear = args[0];
    KeyFactory fact = KeyFactory.getInstance("RSA");
    Cipher objCipherTunkicloud = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    // encrypt side
    objCipherTunkicloud.init(Cipher.ENCRYPT_MODE, fact.generatePublic(new X509EncodedKeySpec(read_file(args[1]))));
    byte[] arrDecryptedKeyBytes = objCipherTunkicloud.doFinal(clear.getBytes(StandardCharsets.UTF_8));
    // for correct result must enable flag and specify 8859-1 on ctor
    String tkn = args.length>3? new String(arrDecryptedKeyBytes,StandardCharsets.ISO_8859_1): new String(arrDecryptedKeyBytes);
    String output = URLEncoder.encode(tkn, "ISO-8859-1");
    System.out.println (output);
    // decrypt side
    String temp = URLDecoder.decode(output, "ISO-8859-1");
    //reused: Cipher objCipherTunkicloud = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    objCipherTunkicloud.init(Cipher.DECRYPT_MODE, fact.generatePrivate(new PKCS8EncodedKeySpec(read_file(args[2]))));
    arrDecryptedKeyBytes = objCipherTunkicloud.doFinal(temp.getBytes(StandardCharsets.ISO_8859_1));
    System.out.println (new String(arrDecryptedKeyBytes));
}

public static byte[] read_file (String filename) throws Exception {
    return Files.readAllBytes(new File(filename).toPath());
}
dave_thompson_085
  • 34,712
  • 6
  • 50
  • 70
  • I added code to test encoding/decoding the byte array as a String using iso-8859-1 and added a paragraph on this solution to my answer. But I think encoding a byte array into a String is a bad idea in the first place and should only be done if there's a very good reason. – sruetti Sep 16 '17 at 23:23