I need to decrypt an AES (PKCS#7) encoded string in my Flutter mobile application.
The string is got from a QR Code, which has been generated from a Java application and contains the AES encoded String.
The Java encoding :
import java.security.Security;
import java.nio.charset.StandardCharsets;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class MyClass {
public static void main(String[] args) throws Exception {
String toEncode = "firstname.lastname#mycompany.com;12";
String encoded = pleaseEncodeMe(toEncode);
System.out.println(encoded);
}
private static String pleaseEncodeMe(String plainText) throws Exception {
Security.addProvider(new BouncyCastleProvider());
final String encryptionAlgorithm = "AES/CBC/PKCS7PADDING";
final String encryptionKey = "WHatAnAWEsoMeKey";
final SecretKeySpec keySpecification = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), encryptionAlgorithm);
final Cipher cipher = Cipher.getInstance(encryptionAlgorithm, "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpecification);
final byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());
return Base64.encodeBase64URLSafeString(encryptedBytes);
}
}
Output : AIRTEuNmSuQtYuysv93w3w83kJJ6sg7kaU7XzA8xrAjOp-lKYPp1brtDAPbhSJmT
The Dart decoding :
void main() {
print(decodeMeOrDie("AIRTEuNmSuQtYuysv93w3w83kJJ6sg7kaU7XzA8xrAjOp-lKYPp1brtDAPbhSJmT"));
}
String decodeMeOrDie(String encryptedString) {
final key = Key.fromUtf8("WHatAnAWEsoMeKey");
final iv = IV.fromLength(16);
final encrypter = Encrypter(AES(key, mode: AESMode.cbc, padding: "PKCS7"));
return encrypter.decrypt64(encryptedString, iv: iv);
}
Output : Y��=X�Rȑ�"Qme#mycompany.com;12
You can see that only a part of the string is decoded.
Two things must be taken into account:
1) For decryption, the IV used for encryption is required.
2) For security reasons, a new IV must be randomly generated for each encryption so that no IV is used more than once with the same key, here.
Therfore, the IV must be passed from the encryption-side to the decryption-side. This doesn't happen automatically, but has to be implemented.
One possibility is to concatenate the byte-arrays of IV and ciphertext. Usually the IV is placed before the ciphertext and the result is Base64-encoded (if required), e.g. in Java:
// Concatenate IV and ciphertext
byte[] iv = ...
byte[] ciphertext = ...
byte[] ivAndCiphertext = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, ivAndCiphertext, 0, iv.length);
System.arraycopy(ciphertext, 0, ivAndCiphertext, iv.length, ciphertext.length);
// If required: Base64-encoding
This data is transmitted to the decryption-side, which separates both parts after Base64-decoding. In the case of AES-CBC, the IV is 16 bytes long, so the first 16 bytes represent the IV and the rest the ciphertext. The IV doesn't need to be encrypted because it isn't secret.
Specifically for your case this means that you have to concatenate IV and ciphertext on the Java-side and to Base64-encode the result. On the Dart-side you have to Base64-decode first and then both parts, IV and ciphertext, can be separated and used for the following decryption.
There are two ways to generate the IV before encryption: Implicit generation by the Cipher-instance as in your example or explicit generation e.g. via SecureRandom. Both alternatives are discussed here. If the IV is generated implicitly (via the Cipher-instance), then this IV must be determined via the Cipher-instance, since it is later required for decryption:
// Determine IV from cipher for later decryption
byte[] iv = cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
If the IV is determined explicitly (e.g. using SecureRandom), it must be passed to the Cipher-instance so that it will be used in the running encryption. This is done using an IvParameterSpec.
// Assign IV to cipher so that it is used for current encryption
byte[] iv = ...
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, secretkeySpec, ivParameterSpec);
A hard-coded key is in general not good practice (except for testing purposes perhaps). However, the topic of key generation/management is outside the scope of this answer. There are already a lot of questions and answers on this subject. If your question is not covered by these answers, please post a new question. A hard-coded IV doesn't occur within the above architecture and should only be used for testing purposes.
If it can help someone, here is the code I ended up with, in dart (it uses the encrypt package) :
/// Decode the specified QR code encrypted string
static String decodeQrCode(String encryptedString) {
try {
// pad the encrypted base64 string with '=' characters until length matches a multiple of 4
final int toPad = encryptedString.length % 4;
if (toPad != 0) {
encryptedString = encryptedString.padRight(encryptedString.length + toPad, "=");
}
// get first 16 bytes which is the initialization vector
final iv = encrypt.IV(Uint8List.fromList(base64Decode(encryptedString).getRange(0, 16).toList()));
// get cipher bytes (without initialization vector)
final encrypt.Encrypted encrypted = encrypt.Encrypted(Uint8List.fromList(
base64Decode(encryptedString).getRange(16, base64Decode(encryptedString).length).toList()));
// decrypt the string using the key and the initialization vector
final key = encrypt.Key.fromUtf8(YOUR_KEY);
final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc, padding: "PKCS7"));
return encrypter.decrypt(encrypted, iv: iv);
} catch (e) {
_log.severe("Error while decoding QR code : $e");
return null;
}
}
Related
I amn trying to replicate the AES-Decrypt function from CyberChef, using GCM mode and HEX input.
Screenshot of the operation:
So far, I've written the following code:
package decryption;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
public class Main {
public static final int GCM_TAG_LENGTH = 32;
public static void main(String[] args) throws Exception {
String IV = "9092d522e11120919fce8492";
String input = "90fab0";
String GCMTag = "02883e111ad6f79cd53674b5f833abab";
String key = "8cda92dcb3283da821daa275359642c7a05d60a4badb5769618193a930c1cdec";
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), 0, key.getBytes(StandardCharsets.UTF_8).length, "AES");
System.out.println(decrypt(input.getBytes(StandardCharsets.UTF_8), secretKey, IV.getBytes(StandardCharsets.UTF_8)));
}
public static String decrypt(byte[] cipherText, SecretKeySpec key, byte[] IV) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, IV);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
byte[] decryptedText = cipher.doFinal(cipherText);
return new String(decryptedText);
}
}
For the above code I am getting:
Exception in thread "main" org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher$InvalidKeyOrParametersException: Invalid value for MAC size: 256
at org.bouncycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineInit(Unknown Source)
at java.base/javax.crypto.Cipher.init(Cipher.java:1442)
at java.base/javax.crypto.Cipher.init(Cipher.java:1375)
at decryption.Main.decrypt(Main.java:29)
at decryption.Main.main(Main.java:19)
Fairly certain I am way off, but I did not find any articles/tutorials on how the GCM Tag can be used as input.
The length of the GCM tag used here is not 32, but 16 bytes.
Furthermore, the BC provider expects ciphertext and tag in concatenated form (ciphertext|tag).
And you have to hex decode key, IV, ciphertext and tag. Since you are running BouncyCastle, you can use org.bouncycastle.util.encoders.Hex.decode(...).
Overall:
import org.bouncycastle.util.encoders.Hex;
...
public static final int GCM_TAG_LENGTH = 16;
...
SecretKeySpec secretKey = new SecretKeySpec(Hex.decode(key), "AES");
System.out.println(decrypt(Hex.decode(input + GCMTag), secretKey, Hex.decode(IV))); // 985
It is not clear from your code whether you are using a static IV/nonce. If so, you should be aware that using a static IV/nonce for GCM is a serious problem, s. e.g. here.
Instead, for each encryption, a random (non-secret) IV/nonce is generated, concatenated with the ciphertext (and tag), and sent together to the decrypting side, which can separate the IV/nonce based on the known IV/nonce size (12 bytes for GCM).
AES/GCM is also supported by the SunJCE provider (at least as of Java 8, s. e.g. here), so you may not need BouncyCastle. For hex decoding you can then use a solution from this post. Starting with Java 17 there is a built-in support.
I'm using openssl's aes-256-cfb algorithm (from NodeJS's crypto module).
While both the NodeJS and the Java code successfully encrypt and decrypt data, the ciphertext is different, even when the iv, key and plaintext are all identical.
openssl/NodeJS cipherText:
05c2aad7bac42ed0846e9a52ce73df9ff9d7ff914feea49fed27d55ad690782a43107914c1b307ec92753227728c95b8e59c546d
Java cipherText:
05C2AAD7BAC42ED084739340D47CEC9F03D8E94AC7B1E11A56A6654F76AD2C8076BCA162303E39B44D043732E98FDD28C52D
I have guessed that openssl's aes-256-cfb translates to Java's AES/CFB/NoPadding.
The ciphertexts both share the same initial 9 bytes, which is odd - I would have expected them to share the first 16 bytes if there had been some difference in the common mode used. (I hope that "common mode" is the collective term for CFB/CBC/CTR/etc.)
Is Java's AES/CFB/NoPadding the correct translation of OpenSSL's aes-256-cfb?
Does the existence of the common first nine bytes imply that Java's AES/CFB/NoPadding is at least using AES256, as opposed to AES128?
If so, what else might account for the differing ciphertext?
Test cases follow:
OpenSSL/NodeJS:
var assert = require('assert');
var crypto = require('crypto');
describe('crypto', function() {
it('should work', function () {
var plainText = new Buffer('a secret plainText which has more than sixteen bytes');
var key = new Buffer('fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210', 'hex');
var iv = new Buffer('0123456789abcdef0123456789abcdef', 'hex');
var cipher = crypto.createCipheriv('aes-256-cfb', key, iv);
var decipher = crypto.createDecipheriv('aes-256-cfb', key, iv);
var cipherText = cipher.update(plainText);
cipher.final(); // no need to use this value with cfb, it seems to always be empty?
assert.equal(plainText.length, cipherText.length);
assert.equal('05c2aad7bac42ed0846e9a52ce73df9ff9d7ff914feea49fed27d55ad690782a43107914c1b307ec92753227728c95b8e59c546d', cipherText.toString('hex'));
var deciphered = decipher.update(cipherText);
decipher.final(); // no need to use value this with cfb, it seems to always be empty?
assert.equal(plainText, deciphered.toString('utf8'));
});
});
Java/Android:
import android.test.InstrumentationTestCase;
import com.google.protobuf.ByteString;
import bit.Twiddling;
import java.security.AlgorithmParameters;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class CryptoTest extends InstrumentationTestCase {
public void test_encrypt_and_decrypt() throws Exception {
byte[] plainText = "a secret message which has more than sixteen bytes".getBytes();
byte[] key = Twiddling.hexStringBEToBytes("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210");
byte[] iv = Twiddling.hexStringBEToBytes("0123456789abcdef0123456789abcdef");//0123456789abcdef0123456789abcdef");
SecretKeySpec aesSecret = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding");
Cipher decipher = Cipher.getInstance("AES/CFB/NoPadding");
IvParameterSpec ivps = new IvParameterSpec(iv);
assertEquals(16, iv.length);
cipher.init(Cipher.ENCRYPT_MODE, aesSecret, ivps);
byte[] cipherText = cipher.doFinal(plainText);
assertEquals(plainText.length, cipherText.length);
assertEquals("05C2AAD7BAC42ED084739340D47CEC9F03D8E94AC7B1E11A56A6654F76AD2C8076BCA162303E39B44D043732E98FDD28C52D", Twiddling.bytesToHexStringBE(cipherText));
decipher.init(Cipher.DECRYPT_MODE, aesSecret, ivps);
byte[] deciphered = decipher.doFinal(cipherText);
assertEquals(new String(plainText), new String(deciphered));
}
}
It was a find/replace error - the two plainTexts differ after the first nine bytes.
Java's name for OpenSSL's aes-256-cfb is AES/CFB/NoPadding.
I'm trying to change encryption algorithm of existing project. But i have a little bit confusion. When i use "PBEWithHmacSHA512AndAES_256" as a parameter, it produce different result but when i use "PBEWithMD5AndDES" as a parameter it produce same result. My functions are :
public static synchronized String encrypt1(final String textToEncrypt, final String pathPublicKey) throws Exception {
final KeySpec pbeKeySpec = new PBEKeySpec(DbKeyHandler.getDbKey(pathPublicKey).toCharArray());
final SecretKey pbeKey = SecretKeyFactory.getInstance("PBEWithMD5AndDES").generateSecret(pbeKeySpec);
// Prepare the parameter to the ciphers
final AlgorithmParameterSpec paramSpec = new PBEParameterSpec(salt, iterationCount);
final Cipher cipher = Cipher.getInstance(pbeKey.getAlgorithm());
// Create the ciphers
cipher.init(Cipher.ENCRYPT_MODE, pbeKey, paramSpec);
// Encode the string into bytes using utf-8
final byte[] utf8 = textToEncrypt.getBytes("UTF8");
// Encrypt
final byte[] enc = cipher.doFinal(utf8);
// Encode bytes to base64 to get a string
return new sun.misc.BASE64Encoder().encode(enc);
}
public static synchronized String encrypt2 (final String textToEncrypt, final String pathPublicKey) throws Exception {
final KeySpec pbeKeySpec = new PBEKeySpec(DbKeyHandler.getDbKey(pathPublicKey).toCharArray());
final SecretKey pbeKey = SecretKeyFactory.getInstance("PBEWithHmacSHA512AndAES_256").generateSecret(pbeKeySpec);
// Prepare the parameter to the ciphers
final AlgorithmParameterSpec paramSpec = new PBEParameterSpec(salt, iterationCount);
final Cipher cipher = Cipher.getInstance(pbeKey.getAlgorithm());
// Create the ciphers
cipher.init(Cipher.ENCRYPT_MODE, pbeKey, paramSpec);
// Encode the string into bytes using utf-8
final byte[] utf8 = textToEncrypt.getBytes("UTF8");
// Encrypt
final byte[] enc = cipher.doFinal(utf8);
// Encode bytes to base64 to get a string
return new sun.misc.BASE64Encoder().encode(enc);
}
Any suggestions, ideas will help me to figure out what's going on here.
Also this is produce different results:
KeyStore keyStore = KeyStore.getInstance("JCEKS");
keyStore.load(new FileInputStream((pathOfJKSfile)), password.toCharArray());
Key key = keyStore.getKey(keyName, keyPass.toCharArray());
byte[] raw = key.getEncoded();
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "PBEWithHmacSHA512AndAES_256");
final AlgorithmParameterSpec paramSpec = new PBEParameterSpec(salt, ITERATIONS);
final Cipher cipherEncrypt = Cipher.getInstance(ALGORITHM);
cipherEncrypt.init(Cipher.ENCRYPT_MODE, secretKeySpec, paramSpec);
final byte[] enc = cipherEncrypt.doFinal(messageBytes);
System.out.println( new sun.misc.BASE64Encoder().encode(enc));
And i know that cipher.init() using "JceSecurity.RANDOM" for pruducing different results.
Both algorithms, PBEWithHmacSHA512AndAES_256 and PBEWithMD5AndDES, first generate an encryption key by processing a password, a salt and an iteration count (using HmacSHA512 and MD5, respectively) and then encrypt the plain text (with AES-256 and DES, respectively) using this key and the CBC-mode. When the Cipher-instance is initialized, a pseudo-random initialization vector (IV) is generated that is required for the CBC- mode.
In the context of PBEWithHmacSHA512AndAES_256, the IV is generated using the SecureRandom implementation of the highest-priority installed provider, at least for the Cipher#init()-method used in the code (note that there are several overloads of the Cipher#init()-method and that a SecureRandom-instance can also be passed explicitly). I.e. with each Cipher-initialization a new (random) IV is generated and therefore the encrypted text is always different, even for an identical plain text. For this reason, the encrypted text in your examples changes in this context.
In the context of PBEWithMD5AndDES, the IV is only determined by the password, the salt, the iteration count (and of course the MD5-hash-algorithm itself). Therefore, the IV and the encrypted text do not change in case of repetition (provided that password, salt, iteration count etc. are the same). For this reason, the encrypted text in your example does not change in this context.
The generation of a new, random IV during the Cipher-initalization makes sense with regard to the following requirements for the IV: For security reasons, an IV in CBC-mode (btw this also applies to other modes) may only be used once under the same key. In addition the IV must be unpredictable.
PBEWithMD5AndDES is deprecated.
EDIT:
The use of an IV is standard nowadays (for security reasons). A lot of information can be found on the Internet on this topic e.g. here. In the following I will only describe a few basic things.
The IV used for encryption must be stored in some way because it is required for decryption. The IV does not have to be kept secret, so that it is usually concatenated with the encrypted data (e.g. before the encrypted data) and stored together with them. During decryption, both parts can be separated because the length of the IV is known (16 Byte for AES). E.g for the concatenation in the encryption-method something like the following is used (let iv and enc be the byte-arrays with the IV and the encrypted data, respectively):
byte[] result = new byte[enc.length + iv.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(enc, 0, result, iv.length, enc.length);
and in the decryption-method the corresponding counterpart (having in mind that the length of an IV in AES is 16 Byte).
In the encryption-method the IV can be determined with Cipher#getIV() (this must of course happen after calling Cipher#init()).
In the decryption-method you have to pass the IV to the PBEParameterSpec-ctor (e.g. let iv be the byte-array with the IV):
IvParameterSpec ivSpec = new IvParameterSpec(iv);
AlgorithmParameterSpec paramSpec = new PBEParameterSpec(salt, iterationCount, ivSpec);
The generation of an IV can also take place outside the Cipher-class, see e.g. Generating random IV for AES in Java. Then you have to pass that IV in the encryption-method in the same way as above described for the decryption-method.
Note, in connection with an IV some points have to be considered e.g. using a mode without an IV (e.g. ECB), using an IV consisting exclusively of 0-values, using a predictable IV or using an IV more than once under the same key etc. drastically reduces security in general, see e.g. here!
I have "inherited" a Ruby on Rails app, and I must translate this app from Ruby to Java, and the most important thing, I don't have contact with the creator.
My problem is with the IV vector in AES-256 authentication. Ruby app uses AESCrypt gem to encrypt and decrypt user's password. It works fine, and I have already some thousands of users in DB.
The problem is when I try to do the same in Java (I've already updated JCE to allow 256bit key lenght). The Key and the IV are writen as binary strings in ruby source code (see bellow), and when I try to use it in Java I get a exception which say that the IV lenght must be 16 bytes long (I know that it must be 16 bytes long, but the binary string in Ruby has 32 characters).
Ruby code (works fine):
require 'openssl'
require 'digest/md5'
require 'base64'
module AESCrypt
KEY = "AB1CD237690AF13B6721AD237A"
IV = "por874hyufijdue7w63ysxwet4320o90"
TYPE = "AES-256-CBC"
def AESCrypt.key(key)
key = Digest::MD5.hexdigest(key)
key.slice(0..32)
end
# Encrypts a block of data given an encryption key and an
# initialization vector (iv). Keys, iv's, and the data returned
# are all binary strings. Cipher_type should be "AES-256-CBC",
# "AES-256-ECB", or any of the cipher types supported by OpenSSL.
# Pass nil for the iv if the encryption type doesn't use iv's (like
# ECB).
#:return: => String
#:arg: data => String
#:arg: key => String
#:arg: iv => String
#:arg: cipher_type => String
def AESCrypt.encrypt(data)
return nil if data.nil?
return data if data.blank?
aes = OpenSSL::Cipher::Cipher.new(TYPE)
aes.encrypt
aes.key = AESCrypt.key(KEY)
aes.iv = IV if IV != nil
result = aes.update(data) + aes.final
Base64.encode64(result)
end
end
and this is my Java code (it should do the same, seems that works with a 16 chars/bytes IV):
public static void main(String[] args) throws UnsupportedEncodingException {
String KEY = "AB1CD237690AF13B6721AD237A";
String IV = "por874hyufijdue7w63ysxwet4320o90";
SecretKeySpec key = generateKey(KEY);
String message = "password";
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] ciphedText = cipher.doFinal(message.getBytes());
String encoded = Base64.encodeBase64String(ciphedText);
System.out.println("ENCRYPTED text= " + encoded);
}
public static SecretKeySpec generateKey(final String password) throws NoSuchAlgorithmException, UnsupportedEncodingException {
final MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] bytes = password.getBytes("UTF-8");
digest.update(bytes, 0, bytes.length);
byte[] key = digest.digest();
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
return secretKeySpec;
}
And I'm getting this exception (obviously):
java.security.InvalidAlgorithmParameterException: Wrong IV length: must be 16 bytes long
at com.sun.crypto.provider.CipherCore.init(CipherCore.java:516)
at com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:339)
at javax.crypto.Cipher.implInit(Cipher.java:801)
at javax.crypto.Cipher.chooseProvider(Cipher.java:859)
at javax.crypto.Cipher.init(Cipher.java:1370)
at javax.crypto.Cipher.init(Cipher.java:1301)
at com.javi.test.security.Test.main(Test.java:129)
I guess my problem is the way I convert the IV java string in byte[]. I think that openSSL code in ruby is unpacking (or doing something internally) the 32 bytes of the IV to 16 bytes. I have tried a lot of things, but I'm going crazy.
Anyone had the same problem or figure out where could be my problem?
I have posted the encryption code but I hace the same issue with decryption.
Thanks in advance, I'll be very grateful with every answer. :)
First, your IV is not actually iv, IV should be HEX encoded, but you have ASCII string "por874hyufijdue7w63ysxwet4320o90", may be it is some how encoded?
Second, IV.getBytes() will transofr IV's each character to hex encoding like p = 0x70, o = 0x6F, r = 0x72, etc...
It is not a useful answer, but may be hint.
Actually IV must be the same length as block cipher single block length. You have 32 bytes long IV itself, if you make IV.getBytes() IV length should match the cipher block length
I have written a small application to encrypt and decrypt Strings using AES. Here is the code:
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class AesEncryptionTest {
static IvParameterSpec initialisationVector = generateInitialisationVector();
static SecretKey encryptionKey = generateKey();
static String plainText = "test text 123\0\0\0";
public static void main(String [] args) {
try {
System.out.println("Initial Plain Text = " + plainText);
byte[] encryptedText = encrypt(plainText, encryptionKey);
System.out.println("Encrypted Text = " + encryptedText);
String decryptedText = decrypt(encryptedText, encryptionKey);
System.out.println("Decrypted Text = " + decryptedText);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] encrypt(String plainText, SecretKey encryptionKey) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", "SunJCE");
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, initialisationVector);
return cipher.doFinal(plainText.getBytes("UTF-8"));
}
public static String decrypt(byte[] encryptedText, SecretKey encryptionKey) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", "SunJCE");
cipher.init(Cipher.DECRYPT_MODE, encryptionKey, initialisationVector);
return new String(cipher.doFinal(encryptedText),"UTF-8");
}
public static SecretKey generateKey() {
SecretKey secretKey = null;
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
secretKey = keyGenerator.generateKey();
} catch (NoSuchAlgorithmException ex) {
// Whine a little
}
return secretKey;
}
public static IvParameterSpec generateInitialisationVector() {
byte[] initVector = new byte[16];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(initVector);
return new IvParameterSpec(initVector);
}
}
Output:
Initial Plain Text = test text 123
Encrypted Text = [B#407dcb32
Decrypted Text = test text 123
My main areas of concern are around encrypting into a byte array and decrypting back to a String. I know that this can introduce unexpected behaviour and loss of data. While this has not been observed in my testing, could anyone suggest any changes that would help combat this? I think I have this covered by ensuring UTF-8 is used both ways.
If anyone see's any other red flags with my code and how I have done this, I'm open to criticism/suggestions.
Many thanks!
You're calling toString() on a byte[] which is never a good idea. Basically it's not giving you any useful information.
If you want to convert arbitrary binary data into a string, I'd suggest using hex or base64, both of which are covered elsewhere. There's no indication that you've actually lost any information here in the encryption/decryption - the problem is your display of the encrypted data. So long as you don't try to treat that as simple encoded text data (because it isn't) you should be fine. In particular, your code is already specifying UTF-8 as the conversion from the original text to unencrypted binary data, and vice versa - so that's safe.
If you don't need to convert the byte array to a string, it's simplest to avoid doing so in the first place. (For example, you could write it to a file still in the binary form very simply, then load it back into a byte array later.)
You asked for other red flags, so I'll give you a few pointers regarding the crypto:
Generally you don't have to provide the provider name when you use an algorithm name. Specifying the provider makes your code less portable.
It is better to use a standardized padding mode such as "/PKCS5Padding" (identical to PKCS#7 padding in Java). If you want to use the current padding mode you can configure the Bouncy Castle provider and specify "/ZeroBytePadding". This padding mode does not work correctly for plaintext that ends with zero valued bytes.
You store the IV in the same class variable as the key. I know this is just test code, but normally the IV need to be send or established at both sides. The most common way to use the same key at both sides is to prefix the IV to the ciphertext.
The size of the IV depends on the cipher. It is always 16 for AES, but you may want to make the IV size configurable or use the Cipher.getBlockSize() method.
Use GCM mode (available since 1.8) encryption if you also want authenticity/integrity and protection against padding oracle attacks.
You should use a fresh, random IV for each encrypt, instead of generating an IV just once.
the way to make sure the conversion is without loss is to use the same Charset when converting back and forth as you do.
Creating a string of the encrypted data is however not safe for further use; it can contain any and all sequences of bytes and might not fit into whatever Charset you originally used (you're not making this error, just pointing it out).
You're also printing the hashcode of the byte[] mid way in the code, not the individual bytes.