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.
0x00
could 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);
}
}