16

I have a hardcoded key with which I want to encrypt a string before storing it in SharedPreferences. This is the code I have so far:

public class TokenEncryptor {

    private final static String TOKEN_KEY = "91a29fa7w46d8x41";

    public static String encrypt(String plain) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            AlgorithmParameterSpec ivSpec = new IvParameterSpec(new byte[16]);
            SecretKeySpec newKey = new SecretKeySpec(TOKEN_KEY.getBytes(), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec);
            return new String(cipher.doFinal(plain.getBytes()));
        } catch (Exception e) {
            Ln.e(e);
            return null;
        }
    }

    public static String decrypt(String encoded) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            AlgorithmParameterSpec ivSpec = new IvParameterSpec(new byte[16]);
            SecretKeySpec newKey = new SecretKeySpec(TOKEN_KEY.getBytes(), "AES");
            cipher.init(Cipher.DECRYPT_MODE, newKey, ivSpec);
            return new String(cipher.doFinal(encoded.getBytes()));
        } catch (Exception e) {
            Ln.e(e);
            return null;
        }
    }
}

It seems to be catching an exception at the end of decrypt method:

javax.crypto.IllegalBlockSizeException: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length

Can someone point me in the right direction? I have a feeling I'm doing something wrong instantiating IvParameterSpec.

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
Oleksiy
  • 37,477
  • 22
  • 74
  • 122
  • 1
    Your key is only 16 bytes long, so you're talking about AES-128 not AES-256. – Artjom B. May 21 '15 at 20:41
  • @ArtjomB. should I just change it to 32 bytes in order to make it AES-256? My apologies for being uninformed about this, it's the first time I have to deal with encryption – Oleksiy May 21 '15 at 20:44
  • Yes, if you increase the key size to 24 or 32 bytes, AES-192 or AES-256 will be used automatically. – Artjom B. May 21 '15 at 20:46
  • @ArtjomB., I am now crashing with `java.security.InvalidAlgorithmParameterException: expected IV length of 16`. I researched that, and it looks like I have to download jars and import them as libraries? http://stackoverflow.com/a/6729947/1367392 Is there not a way to encrypt with AES-256 without that? Thanks – Oleksiy May 21 '15 at 21:06
  • 1
    The IV has to be of the same size as the block length which is 16 bytes for AES (-128, -192 and -256). – Artjom B. May 21 '15 at 21:08
  • Note that usually a random IV is generated to ensure that if the same plaintext is encrypted with the same key, an attacker that observes the ciphertexts cannot reliably say that it is the same plaintext. This is called semantic security. Since the IV doesn't have to be secret, you can safely prepend it to the ciphertext when you send it somewhere. – Artjom B. May 21 '15 at 21:18
  • @ArtjomB., do you mean that instead of hardcoded `TOKEN_KEY`, I should be using a randomly generated key? And if I store it in memory, wouldn't it be a security risk? Could you give an example in an answer so I can get an understanding of this process? Thanks a lot. – Oleksiy May 21 '15 at 21:24
  • No, I'm not talking about the key. I'm talking about the IV which should be randomly generated and prepended to the ciphertext. – Artjom B. May 21 '15 at 21:27
  • @ArtjomB. I think I got it. It seems to work. Please let me know if I forgot something or coded it wrong: http://stackoverflow.com/a/30384640/1367392 Much appreciated! I found plenty of similar problems on the internet, but not something exactly the same. – Oleksiy May 21 '15 at 22:02

4 Answers4

16

When you encrypt a string with AES, you get an array of bytes back. Trying to convert those bytes directly to a string (new String(cipher.doFinal(plaintextBytes))) will cause all sorts of problems. If you require the output from your encryption method to be a string, then use Base64 rather than attempting a direct conversion. In your decryption method, convert the Base64 string back into a byte array before decrypting the byte array.

Also, do not use getBytes() since the output depends on the system defaults. Use getBytes("utf-8") or whatever. That eliminates ambiguity.

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
rossum
  • 15,344
  • 1
  • 24
  • 38
14

Just in case anyone is interested (or feels too lazy to do their research), here is the the result code for AES-256 encryption and decryption that I put together, with help from the accepted answer and comments:

public class TokenEncryptor {

    private final static String TOKEN_KEY = "fqJfdzGDvfwbedsKSUGty3VZ9taXxMVw";

    public static String encrypt(String plain) {
        try {
            byte[] iv = new byte[16];
            new SecureRandom().nextBytes(iv);
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(TOKEN_KEY.getBytes("utf-8"), "AES"), new IvParameterSpec(iv));
            byte[] cipherText = cipher.doFinal(plain.getBytes("utf-8"));
            byte[] ivAndCipherText = getCombinedArray(iv, cipherText);
            return Base64.encodeToString(ivAndCipherText, Base64.NO_WRAP);
        } catch (Exception e) {
            Ln.e(e);
            return null;
        }
    }

    public static String decrypt(String encoded) {
        try {
            byte[] ivAndCipherText = Base64.decode(encoded, Base64.NO_WRAP);
            byte[] iv = Arrays.copyOfRange(ivAndCipherText, 0, 16);
            byte[] cipherText = Arrays.copyOfRange(ivAndCipherText, 16, ivAndCipherText.length);

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(TOKEN_KEY.getBytes("utf-8"), "AES"), new IvParameterSpec(iv));
            return new String(cipher.doFinal(cipherText), "utf-8");
        } catch (Exception e) {
            Ln.e(e);
            return null;
        }
    }

    private static byte[] getCombinedArray(byte[] one, byte[] two) {
        byte[] combined = new byte[one.length + two.length];
        for (int i = 0; i < combined.length; ++i) {
            combined[i] = i < one.length ? one[i] : two[i - one.length];
        }
        return combined;
    }

}
Artjom B.
  • 61,146
  • 24
  • 125
  • 222
Oleksiy
  • 37,477
  • 22
  • 74
  • 122
  • I'm sorry, but why do we have a line "new SecureRandom().nextBytes(iv);" ? The return value is not used, right? – zkvarz Mar 15 '17 at 14:44
  • 2
    @zkvarz: the method manipulates the assigned byte array --> there is no return value – luckyhandler Apr 03 '17 at 13:52
  • @Oleksiy Thanks, it saved my life, here's kotlin version of your answer https://gist.github.com/kasim1011/a5a9644a60c33a4df3c29f4b34cf93a4 – Kasim Rangwala Aug 12 '18 at 11:28
  • I am working with a SecretKey generated with secureRandom, size 128, encodedToString() and then decoded with Base64.NO_WRAP, the Cypher uses AES/CBC/PKCS5Padding , IvParameterSpec lenght 16 with SecureRandom.getInstance("SHA1PRNG") ..., and I cannot find absolutely any answer about what's going wrong. – Delark Jan 26 '21 at 01:13
11

It's an extension of Artjom B answer and working for me.

public String encryptMsg(String message, SecretKey secret)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidParameterSpecException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException {
        Cipher cipher = null;
        cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secret);
        byte[] cipherText = cipher.doFinal(message.getBytes("UTF-8"));
        return Base64.encodeToString(cipherText, Base64.NO_WRAP);
    }

public String decryptMsg(String cipherText, SecretKey secret)
        throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidParameterSpecException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {
    Cipher cipher = null;
    cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.DECRYPT_MODE, secret);
    byte[] decode = Base64.decode(cipherText, Base64.NO_WRAP);
    String decryptString = new String(cipher.doFinal(decode), "UTF-8");
    return decryptString;
}
Sam Reyes
  • 329
  • 4
  • 9
1

Kotlin version of @Oleksiy 's answer.

<script src="https://gist.github.com/kasim1011/a5a9644a60c33a4df3c29f4b34cf93a4.js"></script>
import android.util.Base64
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

private const val algorithm = "AES"
private const val tokenKey = "fqJfdzGDvfwbedsKSUGty3VZ9taXxMVw"
private const val padding = "AES/CBC/PKCS5Padding"
private const val ivSize = 16

fun String.encryptAES(): String {
    val tokenBytes = tokenKey.toByteArray(Charsets.UTF_8)
    val secretKey = SecretKeySpec(tokenBytes, algorithm)

    val ivByteArray = ByteArray(ivSize)
    val iv = IvParameterSpec(ivByteArray)

    val cipher = Cipher.getInstance(padding)
    cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv)

    val cipherText = cipher.doFinal(toByteArray(Charsets.UTF_8))
    val ivAndCipherText = getCombinedArray(ivByteArray, cipherText)

    return Base64.encodeToString(ivAndCipherText, Base64.NO_WRAP)
}

fun String.decryptAES(): String {
    val tokenBytes = tokenKey.toByteArray(Charsets.UTF_8)
    val secretKey = SecretKeySpec(tokenBytes, algorithm)

    val ivAndCipherText = Base64.decode(this, Base64.NO_WRAP)
    val cipherText = Arrays.copyOfRange(ivAndCipherText, ivSize, ivAndCipherText.size)

    val ivByteArray = Arrays.copyOfRange(ivAndCipherText, 0, ivSize)
    val iv = IvParameterSpec(ivByteArray)

    val cipher = Cipher.getInstance(padding)
    cipher.init(Cipher.DECRYPT_MODE, secretKey, iv)

    return cipher.doFinal(cipherText).toString(Charsets.UTF_8)
}

private fun getCombinedArray(one: ByteArray, two: ByteArray): ByteArray {
    val combined = ByteArray(one.size + two.size)
    for (i in combined.indices) {
        combined[i] = if (i < one.size) one[i] else two[i - one.size]
    }
    return combined
}
Kasim Rangwala
  • 1,765
  • 2
  • 23
  • 44