@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 可以标记字符串的结尾,0x0a 和 0x0d 可以被操纵,控制字符被解释。
-
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);
}
}