Simple Java AES encrypt/decrypt example

2018-12-31 17:43发布

问题:

What\'s wrong with the following example?

The problem is that the first part of the decrypted string is nonsense. However, the rest is fine, I get...

Result: `£eB6O�geS��i are you? Have a nice day.
@Test
public void testEncrypt() {
  try {
    String s = \"Hello there. How are you? Have a nice day.\";

    // Generate key
    KeyGenerator kgen = KeyGenerator.getInstance(\"AES\");
    kgen.init(128);
    SecretKey aesKey = kgen.generateKey();

    // Encrypt cipher
    Cipher encryptCipher = Cipher.getInstance(\"AES/CBC/PKCS5Padding\");
    encryptCipher.init(Cipher.ENCRYPT_MODE, aesKey);

    // Encrypt
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, encryptCipher);
    cipherOutputStream.write(s.getBytes());
    cipherOutputStream.flush();
    cipherOutputStream.close();
    byte[] encryptedBytes = outputStream.toByteArray();

    // Decrypt cipher
    Cipher decryptCipher = Cipher.getInstance(\"AES/CBC/PKCS5Padding\");
    IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());
    decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

    // Decrypt
    outputStream = new ByteArrayOutputStream();
    ByteArrayInputStream inStream = new ByteArrayInputStream(encryptedBytes);
    CipherInputStream cipherInputStream = new CipherInputStream(inStream, decryptCipher);
    byte[] buf = new byte[1024];
    int bytesRead;
    while ((bytesRead = cipherInputStream.read(buf)) >= 0) {
        outputStream.write(buf, 0, bytesRead);
    }

    System.out.println(\"Result: \" + new String(outputStream.toByteArray()));

  } 
  catch (Exception ex) {
    ex.printStackTrace();
  }
}

回答1:

Lot of people including myself face lot of issues in making this work due to missing some information like, forgetting to convert to Base64, initialization vectors, character set, etc. So I thought of making a fully functional code.

Hope this will be useful to you all: To compile you need additional Apache Commons Codec jar, which is available here: http://commons.apache.org/proper/commons-codec/download_codec.cgi

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class Encryptor {
    public static String encrypt(String key, String initVector, String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes(\"UTF-8\"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(\"UTF-8\"), \"AES\");

            Cipher cipher = Cipher.getInstance(\"AES/CBC/PKCS5PADDING\");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());
            System.out.println(\"encrypted string: \"
                    + Base64.encodeBase64String(encrypted));

            return Base64.encodeBase64String(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static String decrypt(String key, String initVector, String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes(\"UTF-8\"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(\"UTF-8\"), \"AES\");

            Cipher cipher = Cipher.getInstance(\"AES/CBC/PKCS5PADDING\");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) {
        String key = \"Bar12345Bar12345\"; // 128 bit key
        String initVector = \"RandomInitVector\"; // 16 bytes IV

        System.out.println(decrypt(key, initVector,
                encrypt(key, initVector, \"Hello World\")));
    }
}


回答2:

Here a solution without Apache Commons Codec\'s Base64:

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class AdvancedEncryptionStandard
{
    private byte[] key;

    private static final String ALGORITHM = \"AES\";

    public AdvancedEncryptionStandard(byte[] key)
    {
        this.key = key;
    }

    /**
     * Encrypts the given plain text
     *
     * @param plainText The plain text to encrypt
     */
    public byte[] encrypt(byte[] plainText) throws Exception
    {
        SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);

        return cipher.doFinal(plainText);
    }

    /**
     * Decrypts the given byte array
     *
     * @param cipherText The data to decrypt
     */
    public byte[] decrypt(byte[] cipherText) throws Exception
    {
        SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);

        return cipher.doFinal(cipherText);
    }
}

Usage example:

byte[] encryptionKey = \"MZygpewJsCpRrfOr\".getBytes(StandardCharsets.UTF_8);
byte[] plainText = \"Hello world!\".getBytes(StandardCharsets.UTF_8);
AdvancedEncryptionStandard advancedEncryptionStandard = new AdvancedEncryptionStandard(
        encryptionKey);
byte[] cipherText = advancedEncryptionStandard.encrypt(plainText);
byte[] decryptedCipherText = advancedEncryptionStandard.decrypt(cipherText);

System.out.println(new String(plainText));
System.out.println(new String(cipherText));
System.out.println(new String(decryptedCipherText));

Prints:

Hello world!
դ;��LA+�ߙb*
Hello world!


回答3:

Looks to me like you are not dealing properly with your Initialization Vector (IV). It\'s been a long time since I last read about AES, IVs and block chaining, but your line

IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());

does not seem to be OK. In the case of AES, you can think of the initialization vector as the \"initial state\" of a cipher instance, and this state is a bit of information that you can not get from your key but from the actual computation of the encrypting cipher. (One could argue that if the IV could be extracted from the key, then it would be of no use, as the key is already given to the cipher instance during its init phase).

Therefore, you should get the IV as a byte[] from the cipher instance at the end of your encryption

  cipherOutputStream.close();
  byte[] iv = encryptCipher.getIV();

and you should initialize your Cipher in DECRYPT_MODE with this byte[] :

  IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

Then, your decryption should be OK. Hope this helps.



回答4:

The IV that your using for decryption is incorrect. Replace this code

//Decrypt cipher
Cipher decryptCipher = Cipher.getInstance(\"AES/CBC/PKCS5Padding\");
IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());
decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

With this code

//Decrypt cipher
Cipher decryptCipher = Cipher.getInstance(\"AES/CBC/PKCS5Padding\");
IvParameterSpec ivParameterSpec = new IvParameterSpec(encryptCipher.getIV());
decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

And that should solve your problem.


Below includes an example of a simple AES class in Java. I do not recommend using this class in production environments, as it may not account for all of the specific needs of your application.

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AES 
{
    public static byte[] encrypt(final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {       
        return AES.transform(Cipher.ENCRYPT_MODE, keyBytes, ivBytes, messageBytes);
    }

    public static byte[] decrypt(final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {       
        return AES.transform(Cipher.DECRYPT_MODE, keyBytes, ivBytes, messageBytes);
    }

    private static byte[] transform(final int mode, final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {
        final SecretKeySpec keySpec = new SecretKeySpec(keyBytes, \"AES\");
        final IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        byte[] transformedBytes = null;

        try
        {
            final Cipher cipher = Cipher.getInstance(\"AES/CTR/NoPadding\");

            cipher.init(mode, keySpec, ivSpec);

            transformedBytes = cipher.doFinal(messageBytes);
        }        
        catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) 
        {
            e.printStackTrace();
        }
        return transformedBytes;
    }

    public static void main(final String[] args) throws InvalidKeyException, InvalidAlgorithmParameterException
    {
        //Retrieved from a protected local file.
        //Do not hard-code and do not version control.
        final String base64Key = \"ABEiM0RVZneImaq7zN3u/w==\";

        //Retrieved from a protected database.
        //Do not hard-code and do not version control.
        final String shadowEntry = \"AAECAwQFBgcICQoLDA0ODw==:ZtrkahwcMzTu7e/WuJ3AZmF09DE=\";

        //Extract the iv and the ciphertext from the shadow entry.
        final String[] shadowData = shadowEntry.split(\":\");        
        final String base64Iv = shadowData[0];
        final String base64Ciphertext = shadowData[1];

        //Convert to raw bytes.
        final byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        final byte[] ivBytes = Base64.getDecoder().decode(base64Iv);
        final byte[] encryptedBytes = Base64.getDecoder().decode(base64Ciphertext);

        //Decrypt data and do something with it.
        final byte[] decryptedBytes = AES.decrypt(keyBytes, ivBytes, encryptedBytes);

        //Use non-blocking SecureRandom implementation for the new IV.
        final SecureRandom secureRandom = new SecureRandom();

        //Generate a new IV.
        secureRandom.nextBytes(ivBytes);

        //At this point instead of printing to the screen, 
        //one should replace the old shadow entry with the new one.
        System.out.println(\"Old Shadow Entry      = \" + shadowEntry);
        System.out.println(\"Decrytped Shadow Data = \" + new String(decryptedBytes, StandardCharsets.UTF_8));
        System.out.println(\"New Shadow Entry      = \" + Base64.getEncoder().encodeToString(ivBytes) + \":\" + Base64.getEncoder().encodeToString(AES.encrypt(keyBytes, ivBytes, decryptedBytes)));
    }
}

Note that AES has nothing to do with encoding, which is why I chose to handle it separately and without the need of any third party libraries.



回答5:

In this answer I choose to approach the \"Simple Java AES encrypt/decrypt example\" main theme and not the specific debugging question because I think this will profit most readers.

This is a simple summary of my blog post about AES encryption in Java so I recommend reading through it before implementing anything. I will however still provide a simple example to use and give some pointers what to watch out for.

In this example I will choose to use authenticated encryption with Galois/Counter Mode or GCM mode. The reason is that in most case you want integrity and authenticity in combination with confidentiality (read more in the blog).

AES-GCM Encryption/Decryption Tutorial

Here are the steps required to encrypt/decrypt with AES-GCM with the Java Cryptography Architecture (JCA). Do not mix with other examples, as subtle differences may make your code utterly insecure.

1. Create Key

As it depends on your use-case, I will assume the simplest case: a random secret key.

SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);
SecretKey secretKey = SecretKeySpec(key, \"AES\");

Important:

  • always use a strong pseudorandom number generator like SecureRandom
  • use 16 byte / 128 bit long key (or more - but more is seldom needed)
  • if you want a key derived from a user password, look into a password hash function (or KDF) with stretching property like PBKDF2 or bcrypt
  • if you want a key derived from other sources, use a proper key derivation function (KDF) like HKDF (Java implementation here). Do not use simple cryptographic hashes for that (like SHA-256).

2. Create the Initialization Vector

An initialization vector (iv) is used so that the same secret key will create different cipher texts.

byte[] iv = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY
secureRandom.nextBytes(iv);

Important:

  • never reuse the same iv with the same key (very important in GCM/CTR mode)
  • the iv must be unpredictable in combination with being unique (ie. use random iv)
  • the iv is not required to be secret
  • always use a strong pseudorandom number generator like SecureRandom
  • 12 byte iv is the correct choice for AES-GCM mode

3. Encrypt with IV and Key

final Cipher cipher = Cipher.getInstance(\"AES/GCM/NoPadding\");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); //128 bit auth tag length
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] cipherText = cipher.doFinal(plainText);

Important:

  • use 16 byte / 128 bit authentication tag (used to verify integrity/authenticity)
  • the authentication tag will be automatically appended to the cipher text (in the JCA implemention)
  • since GCM behaves like a stream cipher, no padding is required
  • use CipherInputStream when encrypting large chunks of data
  • want additional (non-secret) data checked if it was changed? You may want to use associated data with cipher.updateAAD(associatedData); More here.

3. Serialize to Single Message

Just append iv and ciphertext. As stated above, the iv doesn\'t need to be secret.

ByteBuffer byteBuffer = ByteBuffer.allocate(4 + iv.length + cipherText.length);
byteBuffer.putInt(iv.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();

Optionally encode with Base64 if you need a string representation. Either use Android\'s or Java 8\'s built-in implementation (do not use Apache Commons Codec - it\'s an awful implementation). Encoding is used to \"convert\" byte arrays to string representation to make it ASCII safe e.g.:

String base64CipherMessage = Base64.getEncoder().encodeToString(cipherMessage);

4. Prepare Decryption: Deserialize

If you have encoded the message, first decode it to byte array:

byte[] cipherMessage = Base64.getDecoder().decode(base64CipherMessage)

then deconstruct the message

ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
int ivLength = byteBuffer.getInt();
if(ivLength < 12 || ivLength >= 16) { // check input parameter
    throw new IllegalArgumentException(\"invalid iv length\");
}
byte[] iv = new byte[ivLength];
byteBuffer.get(iv);
byte[] cipherText = new byte[byteBuffer.remaining()];
byteBuffer.get(cipherText);

Important:

  • be careful to validate input parameters, so to avoid denial of service attacks by allocating too much memory (e.g. an attacker may change the length value to e.g. 2³¹ allocating 2GB of heap)

5. Decrypt

Initialize the cipher and set the same parameters as with the encryption:

final Cipher cipher = Cipher.getInstance(\"AES/GCM/NoPadding\");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, \"AES\"), new GCMParameterSpec(128, iv));
byte[] plainText= cipher.doFinal(cipherText);

Important:

  • don\'t forget to add associated data with cipher.updateAAD(associatedData); if you added it during encryption.

Note that most recent Android (SDK 21+) and Java (7+) implementations should have AES-GCM. Older versions may lack it. I still choose this mode, since it is easier to implement in addition to being more efficient compared to similar mode of Encrypt-then-Mac (with e.g. AES-CBC + HMAC). See this article on how to implement AES-CBC with HMAC.



回答6:

It\'s often the good idea to rely on standard library provided solution:

private static void stackOverflow15554296()
    throws
        NoSuchAlgorithmException, NoSuchPaddingException,        
        InvalidKeyException, IllegalBlockSizeException,
        BadPaddingException
{

    // prepare key
    KeyGenerator keygen = KeyGenerator.getInstance(\"AES\");
    SecretKey aesKey = keygen.generateKey();
    String aesKeyForFutureUse = Base64.getEncoder().encodeToString(
            aesKey.getEncoded()
    );

    // cipher engine
    Cipher aesCipher = Cipher.getInstance(\"AES/ECB/PKCS5Padding\");

    // cipher input
    aesCipher.init(Cipher.ENCRYPT_MODE, aesKey);
    byte[] clearTextBuff = \"Text to encode\".getBytes();
    byte[] cipherTextBuff = aesCipher.doFinal(clearTextBuff);

    // recreate key
    byte[] aesKeyBuff = Base64.getDecoder().decode(aesKeyForFutureUse);
    SecretKey aesDecryptKey = new SecretKeySpec(aesKeyBuff, \"AES\");

    // decipher input
    aesCipher.init(Cipher.DECRYPT_MODE, aesDecryptKey);
    byte[] decipheredBuff = aesCipher.doFinal(cipherTextBuff);
    System.out.println(new String(decipheredBuff));
}

This prints \"Text to encode\".

Solution is based on Java Cryptography Architecture Reference Guide and https://stackoverflow.com/a/20591539/146745 answer.



回答7:

Online Editor Runnable version:-

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
//import org.apache.commons.codec.binary.Base64;
import java.util.Base64;

public class Encryptor {
    public static String encrypt(String key, String initVector, String value) {
        try {
            Cipher cipher = Cipher.getInstance(\"AES/CBC/PKCS5PADDING\");
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes(\"UTF-8\"));

            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(\"UTF-8\"), \"AES\");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());

            //System.out.println(\"encrypted string: \"
              //      + Base64.encodeBase64String(encrypted));

            //return Base64.encodeBase64String(encrypted);
            String s = new String(Base64.getEncoder().encode(encrypted));
            return s;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static String decrypt(String key, String initVector, String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes(\"UTF-8\"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(\"UTF-8\"), \"AES\");

            Cipher cipher = Cipher.getInstance(\"AES/CBC/PKCS5PADDING\");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.getDecoder().decode(encrypted));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) {
        String key = \"Bar12345Bar12345\"; // 128 bit key
        String initVector = \"RandomInitVector\"; // 16 bytes IV

        System.out.println(encrypt(key, initVector, \"Hello World\"));
        System.out.println(decrypt(key, initVector, encrypt(key, initVector, \"Hello World\")));
    }
}


回答8:

This is an improvement over the accepted answer.

Changes:

(1) Using random IV and prepend it to the encrypted text

(2) Using SHA-256 to generate a key from a passphrase

(3) No dependency on Apache Commons

public static void main(String[] args) throws GeneralSecurityException {
    String plaintext = \"Hello world\";
    String passphrase = \"My passphrase\";
    String encrypted = encrypt(passphrase, plaintext);
    String decrypted = decrypt(passphrase, encrypted);
    System.out.println(encrypted);
    System.out.println(decrypted);
}

private static SecretKeySpec getKeySpec(String passphrase) throws NoSuchAlgorithmException {
    MessageDigest digest = MessageDigest.getInstance(\"SHA-256\");
    return new SecretKeySpec(digest.digest(passphrase.getBytes(UTF_8)), \"AES\");
}

private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
    return Cipher.getInstance(\"AES/CBC/PKCS5PADDING\");
}

public static String encrypt(String passphrase, String value) throws GeneralSecurityException {
    byte[] initVector = new byte[16];
    SecureRandom.getInstanceStrong().nextBytes(initVector);
    Cipher cipher = getCipher();
    cipher.init(Cipher.ENCRYPT_MODE, getKeySpec(passphrase), new IvParameterSpec(initVector));
    byte[] encrypted = cipher.doFinal(value.getBytes());
    return DatatypeConverter.printBase64Binary(initVector) +
            DatatypeConverter.printBase64Binary(encrypted);
}

public static String decrypt(String passphrase, String encrypted) throws GeneralSecurityException {
    byte[] initVector = DatatypeConverter.parseBase64Binary(encrypted.substring(0, 24));
    Cipher cipher = getCipher();
    cipher.init(Cipher.DECRYPT_MODE, getKeySpec(passphrase), new IvParameterSpec(initVector));
    byte[] original = cipher.doFinal(DatatypeConverter.parseBase64Binary(encrypted.substring(24)));
    return new String(original);
}