【问题标题】:error when decrypting java解密java时出错
【发布时间】:2018-02-24 22:32:47
【问题描述】:

我在解密时遇到问题;我收到以下错误:

javax.crypto.BadPaddingException:解密错误

我的加密代码如下:

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");

解密是这样的:

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);

问题是什么,我该如何解决?

解密时在这一行出现错误:

byte[] arrDecryptedKeyBytes = objCipherTunkicloud.doFinal(userName.getBytes(StandardCharse‌​ts.ISO_8859_1));

【问题讨论】:

  • new String(arrDecryptedKeyBytes) 没有意义。加密生成二进制数据,即不代表平台默认编码中的字符的随机字节。不要将这些字节转换为字符串,或者通过对字节进行 base64 编码来实现。
  • 解密时出现此行的错误:byte[] arrDecryptedKeyBytes = objCipherTunkicloud.doFinal(userName.getBytes(StandardCharsets.ISO_8859_1));
  • 一般来说,bad padding error 意味着解密失败通常是由于不正确的密钥或数据,错误可能是不正确的编码。
  • @JBNizet:使用默认字符集进行转换是不安全的,但如果您随后保留所有 8859-1 字符,则转换为 8859-1 是安全的,这段代码会这样做(尽管很奇怪)
  • @dave_thompson_085:String tkn = new String(arrDecryptedKeyBytes);。那不是使用 ISO-8859-1(或者它是偶然使用的)。

标签: java encryption


【解决方案1】:

@JB Nizet 的评论解释并解决了您的问题。我会详细说明一下:

理论

  • (良好)加密的结果是随机的,例如一个随机字节数组。因此,结果将包含所有可能的字节组合
  • String(byte[]) 将给定的字节数组解释为默认(或给定)编码中的字符数据
  • 并非所有字节或字节序列都代表(取决于编码)有效字符。无效字节/序列的行为未定义 - 它们可能只是被忽略
  • 因此,String(byte[] encrypted).getBytes() 不会为所有可能的字节数组返回encrypted
  • 因此,您的代码会因某些输入而失败。
  • Base64 (java.util.Base64) 通常用于打印加密结果。
  • 某些加密算法仅适用于特定长度的块Cipher 负责处理此问题并根据需要填充您的输入。
  • 如果您的编码/解码周期丢失字符,则要解码的字节不再与所需的块大小对齐,您会得到 BadPaddingException

使用 ISO-8859-1 / Latin1

正如@dave_thompson_085 指出的那样,如果您强制java 使用String(encrypted, StandardCharsets.ISO_8859_1)encrypted.getBytes(StandardCharsets.ISO_8859_1) 将字节数组解释为ISO-8859-1 (latin1),则可以将字节数组转换为字符串并返回而不会丢失。 ISO-8859-1 映射所有 256 字节值并且没有任何“无效值”。我在代码中添加了相应的编码器/解码器。

但是:确保您知道走这条路时的后果

  • 字节数组只是不是字符串!滥用类型有很多副作用。
  • 当您尝试在另一个程序中 读取此字符串时,您必须确保您的目标系统(以及正在进行的所有操作)对如何处理 Latin1 有相同的想法。 0x00 可以标记字符串的结尾,0x0a0x0d 可以被操纵,控制字符被解释。
  • Base64 通常用于加密文本是有原因的...

代码

您的代码做了很多不同的事情。尤其是对于密码学,分离关注点独立测试它们通常是值得的。为了重现和解决问题,我做了一些更改:

  • 我省略了URLDecode / URLEncode,因为它不会导致问题(并且可能属于另一层...)。
  • 我们无权访问您的loadPublicKey("/keylecordonbleu/public.key", "RSA") 方法和您的密钥文件...我将其替换为KeyPair 我在每次测试中生成。您可能希望从这里开始,并在其他代码正常工作后添加您的密钥。
  • 我提取了代码,将加密的byte[] 编码为String,并将String 解码为byte[] 以进行解密。

这可以让你:

  • 测试裸加密/解密周期encryptDecryptRoundtrip(String userName),无需编码为字符串。这已经在您的代码中起作用了。
  • 测试你的 byte[] -> String -> byte[] encoding (testSimpleEncoder()) 这显然不适用于所有输入,并验证同样适用于 Base64 编码 (@ 987654342@)。

使用您的解密/加密代码进行分类:

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);
  }
}

测试类:

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);
  }
}

【讨论】:

    【解决方案2】:

    看起来问题是您使用doFinal(userName.getBytes(StandardCharsets.UTF_8)); 加密并使用doFinal(userName.getBytes(StandardCharsets.ISO_8859_1)); 解密

    【讨论】:

    • 这是一个问题,但即使他解决了问题,他仍然会遇到@JB Nizet 的评论指出的问题。
    【解决方案3】:

    您对密文使用 URL 编码是不寻常的、低效的,并且通常可能不安全,但 Java 实现的 URL 编码可以保留 8859-1 的所有 256 个字符 如果您正确创建它们 .如果提供了第四个参数导致new String(encryptedbytes) 指定 8859-1 而不是默认为 JVM 的默认字符集(因平台和环境而异,通常不是 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());
    }
    

    【讨论】:

    • 我添加了代码来测试使用 iso-8859-1 将字节数组编码/解码为字符串,并在我的答案中添加了关于此解决方案的段落。但我认为首先将字节数组编码为字符串是一个坏主意,只有在有充分理由的情况下才应该这样做。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-02
    相关资源
    最近更新 更多