Decrypt in chunks a AES 128 CBC encrypted object - aes

I have an Encrypted object in Minio, encrypted using the AES 128 bit CBC algorithm.
The object is quite large (~50 MB) so instead of loading it into the memory completely (which may cause out of memory exception), I am retrieving it in chunks of 1MB. I need to decrypt it before use.
Is it possible to decrypt the object in this way (1MB at a time, the whole object was encrypted in one go)?
If yes, how can I do it?
I have tried decrypting 16-byte chunks which produce the following errors:
javax.crypto.BadPaddingException: Given final block not properly padded
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher

To avoid an "out of memory error" you want to decrypt a large (encrypted) file in chunks of 1 mb size - yes it's possible with AES CBC mode.
Below you find a complete example that is generating a sample plaintext file ('plaintext.dat') with random content with the size of 50 mb + 1 byte (the + 1 byte is good to test for file sizes that are not exact multiples of 16 = AES blocksize).
In the next step this file is getting encrypted to 'ciphertext.dat' using a randomly created initialization vector and key.
The last step is the requested decryption method - it decrypts the encrypted file in chunks of 1 mb and in the lines '// obuf holds the decrypted chunk, do what you want to do with the data' and '// final data' you do have the decrypted data in the byte array obuf. For testing I'm writing the decrypted data to the file 'decryptedtext.dat' in appending mode (for that reason this file is deleted in the beginning if it exists).
To prove that decryption was successful I'm comparing the SHA256-hashes of plaintext- and decryptedtext-files.
Two notes: I'm using a 32 byte = 256 bit long key for AES CBC 256. This program has no proper exception handling and is for educational purposes only.
result:
decrypt AES CBC 256 in 1 mb chunks
file with random data created: plaintext.dat
encryption to ciphertext.dat was successfull: true
decryption in chunks of 1 mb
decrypted file written to decryptedtext.dat
plaintext equals decrytedtext file: true
code:
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.file.Files;
import java.security.*;
import java.util.Arrays;
public class AES_CBC_chunk_decryption {
public static void main(String[] args) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException,
InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException {
System.out.println("https://stackoverflow.com/questions/63325528/decrypt-in-chunks-a-aes-128-cbc-encrypted-object/63325529#63325529");
System.out.println("decrypt AES CBC 256 in 1 mb chunks");
// setup for creation of a 50mb encrypted file
int filesize = (50 * 1024 * 1024) + 1; // 50 mb + 1 byte = 52428801 bytes
String filenamePlaintext = "plaintext.dat";
String filenameCiphertext = "ciphertext.dat";
String filenameDecryptedtext = "decryptedtext.dat";
File file = new File("plaintext.dat");
// fill with random bytes.
try (FileOutputStream out = new FileOutputStream(file)) {
byte[] bytes = new byte[filesize];
new SecureRandom().nextBytes(bytes);
out.write(bytes);
}
System.out.println("\nfile with random data created: " + filenamePlaintext);
// delete decrypted file if it exists
Files.deleteIfExists(new File(filenameDecryptedtext).toPath());
// setup random key & iv
SecureRandom secureRandom = new SecureRandom();
byte[] iv = new byte[16];
byte[] key = new byte[32]; // I'm using a 32 byte = 256 bit long key for aes 256
secureRandom.nextBytes(iv);
secureRandom.nextBytes(key);
// encrypt complete file
boolean resultEncryption = encryptCbcFileBufferedCipherOutputStream(filenamePlaintext, filenameCiphertext, key, iv);
System.out.println("encryption to " + filenameCiphertext + " was successfull: " + resultEncryption);
// encrypted file is 52428816 bytes long
System.out.println("\ndecryption in chunks of 1 mb");
// decryption in chunks of 1 mb
try (FileInputStream in = new FileInputStream(filenameCiphertext)) {
byte[] ibuf = new byte[(1024 * 1024)]; // chunks of 1 mb
int len;
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
while ((len = in.read(ibuf)) != -1) {
byte[] obuf = cipher.update(ibuf, 0, len);
if (obuf != null)
// obuf holds the decrypted chunk, do what you want to do with the data
// I'm writing it to a file in appending mode
try (FileOutputStream output = new FileOutputStream(filenameDecryptedtext, true)) {
output.write(obuf);
}
}
byte[] obuf = cipher.doFinal();
if (obuf != null)
// final data
try (FileOutputStream output = new FileOutputStream(filenameDecryptedtext, true)) {
output.write(obuf);
}
}
System.out.println("decrypted file written to " + filenameDecryptedtext);
System.out.println("plaintext equals decrytedtext file: " + filecompareSha256Large(filenamePlaintext, filenameDecryptedtext));
}
public static boolean encryptCbcFileBufferedCipherOutputStream(String inputFilename, String outputFilename, byte[] key, byte[] iv)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
try (FileInputStream in = new FileInputStream(inputFilename);
FileOutputStream out = new FileOutputStream(outputFilename);
CipherOutputStream encryptedOutputStream = new CipherOutputStream(out, cipher);) {
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] buffer = new byte[8096];
int nread;
while ((nread = in.read(buffer)) > 0) {
encryptedOutputStream.write(buffer, 0, nread);
}
encryptedOutputStream.flush();
}
if (new File(outputFilename).exists()) {
return true;
} else {
return false;
}
}
public static boolean filecompareSha256Large(String filename1, String filename2) throws IOException, NoSuchAlgorithmException {
boolean result = false;
byte[] hash1 = generateSha256Buffered(filename1);
byte[] hash2 = generateSha256Buffered(filename2);
result = Arrays.equals(hash1, hash2);
return result;
}
private static byte[] generateSha256Buffered(String filenameString) throws IOException, NoSuchAlgorithmException {
// even for large files
byte[] buffer = new byte[8192];
int count;
MessageDigest md = MessageDigest.getInstance("SHA-256");
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filenameString));
while ((count = bis.read(buffer)) > 0) {
md.update(buffer, 0, count);
}
bis.close();
return md.digest();
}
}

Yes, with AES-128-CBC, it is possible to decrypt just a single block of cyphertext. Each block is 128 bits (16 bytes).
See the diagram at https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC). As you can see, to decrypt any block of ciphertext, you AES-decrypt the block of ciphertext, then XOR the plaintext with the previous block of ciphertext. (For the first block, the plaintext is XOR'd with the IV).
The library that you are using is probably throwing these exceptions, because it is checking if the decrypted ciphertext is properly padded. Of course, if you are decrypting just one arbitrary block of ciphertext, it will not have the proper padding. However, you can use a tool like openssl to decrypt a single block of ciphertext, given the ciphertext, the key, and the previous block of ciphertext, like so:
echo -n 'bc6d8afc78e805b7ed7551e42da4d877' | xxd -p -r | openssl aes-128-cbc -d -nopad -K e3e33d2d9591b462c55503f7ec697839 -iv 1d3fa2b7c9008e1cdbc76a1f22388b89
where:
bc6d8afc78e805b7ed7551e42da4d877 is the block of ciphertext that you want to decrypt
e3e33d2d9591b462c55503f7ec697839 is the key
1d3fa2b7c9008e1cdbc76a1f22388b89 is the previous block of ciphertext

Yes, it is possible. However, due to the mode and padding it may be trickier to program than it looks at first sight.
However, I've created a class that will happily decode from any offset and to any size. Note that the ciphertext should not contain the IV.
In hindsight I might better have used ByteBuffer to make it a bit more flexible, but yeah, that will require an entire rewrite...
package com.stackexchange.so;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* A class that helps you to partially decrypt a CBC ciphertext. Although this class helps you to partially decrypt any
* part, you'd probably want to decrypt chunks that consists of a specific number of blocks; both the <code>off</code>
* and <code>len</code> parameter should be a modulus the block size. If you know the exact plaintext length then you
* can size the last chunk precisely.
*
* #author maartenb
*/
public class CBCDecryptByOffset {
private enum State {
UNINITIALIZED, INITIALIZED, RUNNING;
};
private final Cipher cbcCipher;
private SecretKey symKey;
private IvParameterSpec iv;
private State state = State.UNINITIALIZED;
/**
* Creates the CBC decryptor class and initializes it.
* #param blockCipher the block cipher, without block cipher mode or padding indication e.g. <code>"AES"</code>
* #throws NoSuchAlgorithmException if the block cipher is not available for <code>"CBC"</code>
* #throws NoSuchPaddingException if the block cipher in CBC mode is not available with <code>"NoPadding"</code>
*/
public CBCDecryptByOffset(String blockCipher) throws NoSuchAlgorithmException, NoSuchPaddingException {
this.cbcCipher = Cipher.getInstance(blockCipher + "/CBC/NoPadding");
}
/**
* Mimics {#link Cipher#init(int, java.security.Key, java.security.spec.AlgorithmParameterSpec)} except that it
* doesn't include options for encryption, wrapping or unwrapping.
*
* #param symKey the key to use
* #param iv the IV to use
* #throws InvalidKeyException if the key is not valid for the block cipher
* #throws InvalidAlgorithmParameterException if the IV is not valid for CBC, i.e. is not the block size
*/
public void init(SecretKey symKey, IvParameterSpec iv)
throws InvalidKeyException, InvalidAlgorithmParameterException {
this.symKey = symKey;
this.iv = iv;
// init directly, probably we want to start here, and it will perform a cursory check of the key and IV
this.cbcCipher.init(Cipher.DECRYPT_MODE, symKey, iv);
this.state = State.INITIALIZED;
}
/**
* Decrypts a partial number of bytes from a CBC encrypted ciphertext with PKCS#7 compatible padding.
*
* #param fullCT the full ciphertext
* #param off the offset within the full ciphertext to start decrypting
* #param len the amount of bytes to decrypt
* #return the plaintext of the partial decryption
* #throws BadPaddingException if the ciphertext is not correctly padded (only checked for the final CT block)
* #throws IllegalBlockSizeException if the ciphertext is empty or not a multiple of the block size
*/
public byte[] decryptFromOffset(byte[] fullCT, int off, int len)
throws BadPaddingException, IllegalBlockSizeException {
if (state == State.UNINITIALIZED) {
throw new IllegalStateException("Instance should be initialized before decryption");
}
int n = cbcCipher.getBlockSize();
if (fullCT.length == 0 || fullCT.length % n != 0) {
throw new IllegalBlockSizeException(
"Ciphertext must be a multiple of the blocksize, and should contain at least one block");
}
if (off < 0 || off > fullCT.length) {
throw new IllegalArgumentException("Invalid offset: " + off);
}
if (len < 0 || off + len < 0 || off + len > fullCT.length) {
throw new IllegalArgumentException("Invalid len");
}
if (len == 0) {
return new byte[0];
}
final int blockToDecryptFirst = off / n;
final int blockToDecryptLast = (off + len - 1) / n;
final int bytesToDecrypt = (blockToDecryptLast - blockToDecryptFirst + 1) * n;
final byte[] pt;
try {
// determine the IV to use
if (state != State.INITIALIZED || off != 0) {
IvParameterSpec vector;
final int blockWithVector = blockToDecryptFirst - 1;
if (blockWithVector == -1) {
vector = iv;
} else {
vector = new IvParameterSpec(fullCT, blockWithVector * n, n);
}
cbcCipher.init(Cipher.DECRYPT_MODE, symKey, vector);
}
// perform the actual decryption (note that offset and length are in bytes)
pt = cbcCipher.doFinal(fullCT, blockToDecryptFirst * n, bytesToDecrypt);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Incorrectly programmed, error should never appear", e);
}
// we need to unpad if the last block is the final ciphertext block
int sigPadValue = 0;
final int finalCiphertextBlock = (fullCT.length - 1) / n;
if (blockToDecryptLast == finalCiphertextBlock) {
int curPaddingByte = bytesToDecrypt - 1;
int padValue = Byte.toUnsignedInt(pt[curPaddingByte]);
if (padValue == 0 || padValue > n) {
throw new BadPaddingException("Invalid padding");
}
for (int padOff = curPaddingByte - 1; padOff > curPaddingByte - padValue; padOff--) {
if (Byte.toUnsignedInt(pt[padOff]) != padValue) {
throw new BadPaddingException("Invalid padding");
}
}
// somebody tries to decrypt just padding bytes
if (off >= (blockToDecryptLast + 1) * n - padValue) {
sigPadValue = len;
} else {
// calculate if any (significant) padding bytes need to be ignored within the plaintext
int bytesInFinalBlock = (off + len - 1) % n + 1;
sigPadValue = padValue - (n - bytesInFinalBlock);
if (sigPadValue < 0) {
sigPadValue = 0;
}
}
}
int ptStart = off - blockToDecryptFirst * n;
int ptSize = len - sigPadValue;
state = State.RUNNING;
if (pt.length == ptSize) {
return pt;
}
return Arrays.copyOfRange(pt, ptStart, ptStart + ptSize);
}
}
Note that I've tested the general functionality but I'd make sure that I wrap it with some JUnit tests if I were you.

Related

Send an encrypted request to APi in Dart

I am trying to send an encrypted request to a specific API in dart, but without success - I don't have any experience with the Dart language.
This are the requirements:
The JSON to be sent is encrypted as follows: "AES/CBC/ZeroBytePadding", IV is generated according to SHA1PRNG with a length of 16 bytes.
The encrypted bytes are Base64 encoded. This results in the encryptedJson.
The hmac is generated from base64 encoded IV and the encryptedJson with "HmacSHA256".
A json will be generated: {"value":encryptedJson,"iv":initialisationVector,"mac":hmac}
This json will be base64 encoded and sent as an encrypted payload.
Can anyone help me? Thanks in advance!
This is the Dart Code so far.
import 'dart:convert';
import 'dart:core';
import 'package:crypto/crypto.dart' as crypto;
import 'package:encrypt/encrypt.dart' as enc;
String encrypt(String string) {
// json encryption
final enc.Key key = enc.Key.fromUtf8(env.get('password'));
final enc.IV iv = enc.IV.fromSecureRandom(IV_LENGTH);
final enc.Encrypter encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc));
final encryptedJson = encrypter.encrypt(string, iv: iv);
final String IVBase64String = base64.encode(iv.bytes);
print('encrypted JSON: '+encryptedJson.base64);
print('decrypted JSON: '+encrypter.decrypt(encryptedJson, iv: iv));
crypto.Hmac hmacSha256 = new crypto.Hmac(crypto.sha256, key.bytes);
crypto.Digest sha256Result = hmacSha256.convert(iv.bytes + encryptedJson.bytes);
print('data: ' + encryptedJson.base64);
print('iv: ' + IVBase64String);
print('hmac: ' + sha256Result.toString());
// Payload
final encryptedText = "{\"value\":\""+encryptedJson.base64+"\",\"iv\":\""+IVBase64String+"\",\"mac\":\""+sha256Result.toString()+"\"}";
print('final: ' + jsonEncode(encryptedText));
return base64.encode(utf8.encode(encryptedText));
}
This is the JavaExample
import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ApiJavaSample
{
private final Cipher cipher;
private final SecretKeySpec key;
private static final String TAG = "AESCrypt";
private static final int IV_LENGTH = 16;
private String cypher_mode = "AES/CBC/NoPadding";
private String cypher_mode_iv = "SHA1PRNG";
public static void main (String[] args)
{
try{
System.out.println("encrypting");
ApiJavaSample test = new ApiJavaSample("password");
String encryptedString = test.encrypt("{\"coupon_key\":\"011205358365\",\"location_id\":\"2\",\"device_key\":\"test_1234\"}");
System.out.println("encrpyted");
System.out.println(encryptedString);
}
catch(Exception e)
{
System.out.println(e);
}
}
public ApiJavaSample(String password) throws Exception
{
// hash password with SHA-256 and crop the output to 128-bit for key
//MessageDigest digest = MessageDigest.getInstance("SHA-256");
//digest.Updater(password.getBytes("UTF-8"));
byte[] keyBytes = password.getBytes();
cipher = Cipher.getInstance(cypher_mode);
key = new SecretKeySpec(keyBytes, "AES");
}
private String hmacDigest(String msg, String algo)
{
String digest = null;
try
{
//SecretKeySpec key = new SecretKeySpec((keyString).getBytes("UTF-8"), algo);
Mac mac = Mac.getInstance(algo);
mac.init(key);
byte[] bytes = mac.doFinal(msg.getBytes("UTF-8"));
StringBuilder hash = new StringBuilder();
for (int i = 0; i < bytes.length; i++)
{
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1)
{
hash.append('0');
}
hash.append(hex);
}
digest = hash.toString();
}
catch (UnsupportedEncodingException | InvalidKeyException e)
{
e.printStackTrace();
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
return digest;
}
public String encrypt(String plainText) throws Exception
{
byte[] iv_bytes = generateIv();
AlgorithmParameterSpec spec = new IvParameterSpec(iv_bytes);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
int blockSize = cipher.getBlockSize();
while (plainText.length() % blockSize != 0) {
plainText += "\0";
}
byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
String encryptedText = Base64.getEncoder().encodeToString(encrypted);
String iv_base64_string = Base64.getEncoder().encodeToString(iv_bytes);
String mac = hmacDigest(iv_base64_string + encryptedText.trim(), "HmacSHA256");
//JSONObject encryptedJson = new JSONObject();
//encryptedJson.put("value", encryptedText.trim());
//encryptedJson.put("iv", iv_base64_string);
//encryptedJson.put("mac", mac);
String base64Encrypt = "{\"value\":\""+encryptedText.trim()+"\",\"iv\":\""+iv_base64_string+"\",\"mac\":\""+mac+"\"}";
return Base64.getEncoder().encodeToString(base64Encrypt.getBytes());
}
private byte[] generateIv() throws NoSuchAlgorithmException
{
SecureRandom random = SecureRandom.getInstance(cypher_mode_iv);
byte[] iv = new byte[IV_LENGTH];
random.nextBytes(iv);
return iv;
}
}
Here is my test data:
Plaintext:
"{\"coupon_key\":\"382236526272\",\"location_id\":\"2\",\"device_key\":\"test_1234\"}"
Key:
33a485cb146e1153c69b588c671ab474
The following has to be changed/optimized in the Dart code:
The Java code uses Zero padding. PointyCastle and the encrypt package (a PointyCastle wrapper) do not support Zero padding (to my knowledge). A possible approach for the Dart code is to disable the default PKCS#7 padding in combination with a custom implementation for Zero padding.
The Java code applies the Base64 encoded data for the HMAC, while the Dart code uses the raw data. This has to be changed.
The Base64 encoding of the IV is obtained more efficiently with iv.base64.
Thus, the code is to be changed as follows:
import 'package:crypto/crypto.dart' as crypto;
import 'package:encrypt/encrypt.dart' as enc;
import 'package:convert/convert.dart';
import 'dart:typed_data';
import 'dart:convert';
String encrypt(String string) {
final enc.Key key = enc.Key.fromUtf8(env.get('password')); // Valid AES key
final enc.IV iv = enc.IV.fromSecureRandom(IV_LENGTH); // IV_LENGTH = 16
final dataPadded = pad(Uint8List.fromList(utf8.encode(string)), 16);
final enc.Encrypter encrypter = enc.Encrypter(enc.AES(key, mode: enc.AESMode.cbc, padding: null));
final encryptedJson = encrypter.encryptBytes(dataPadded, iv: iv);
crypto.Hmac hmacSha256 = crypto.Hmac(crypto.sha256, key.bytes);
crypto.Digest sha256Result = hmacSha256.convert(utf8.encode(iv.base64 + encryptedJson.base64));
final encryptedText = "{\"value\":\""+encryptedJson.base64+"\",\"iv\":\""+iv.base64+"\",\"mac\":\""+sha256Result.toString()+"\"}";
return base64.encode(utf8.encode(encryptedText));
}
Uint8List pad(Uint8List plaintext, int blockSize){
int padLength = (blockSize - (plaintext.lengthInBytes % blockSize)) % blockSize;
if (padLength != 0) {
BytesBuilder bb = BytesBuilder();
Uint8List padding = Uint8List(padLength);
bb.add(plaintext);
bb.add(padding);
return bb.toBytes();
}
else {
return plaintext;
}
}
Test (using a static IV to allow comparison between the ciphertexts of the two codes):
Key: enc.Key.fromUtf8("5432109876543210")
IV: enc.IV.fromUtf8("0123456789012345")
Plaintext: "{\"coupon_key\":\"011205358365\",\"location_id\":\"2\",\"device_key\":\"test_1234\"}"
Result: eyJ2YWx1ZSI6InNRTjJ0OWc5ZWY2RzdNV2RsOFB3emlXSlQwclNxUWJ2ZnN0eCtpMmNtSTQyUXJjUGRNV0JLbTlRZ2kxdmM0dElna2NOZEJsOVpEM0JlYTFPZ1kxaHNSeklSbHM1TnlaN0s1T2NqMTEzdkdvPSIsIml2IjoiTURFeU16UTFOamM0T1RBeE1qTTBOUT09IiwibWFjIjoiMzkwYzlhMzAxMjAxYjc1MWUxNjBhM2JlZTdmZGU5YzE5ZDY0MzJlNTBjOTJhNTg0ODBhMTJkNTYyNWRkYWMyNSJ9
After the changes, both codes return the above result for the above input data.
Security:
Typically, an AES key is a randomly generated byte sequence and not a string. If the key is to be derived from a passphrase/string, a reliable key derivation like PBKDF2 is to be used.
Zero padding is unreliable, so the reliable PKCS#7 padding that most libraries use by default should be applied. If the Java code had used PKCS#7 padding, porting would have been easier.
For encoding/decoding the charset should be specified (e.g. getBytes(StandardCharsets.UTF_8)), otherwise the default encoding will be used (which might not be wanted).
Using the same key for encryption and integrity checking for AES/HMAC is not a pressing security issue, but should be avoided as a preventive measure, see here.
The code is partially inefficient, e.g. when concatenating the Base64 encoded data instead of the raw data to determine the HMAC.

AES GCM in PHP/Java

I have to write the encryption/decryption library in AES-256 with GCM block mode.
I have written the same in java and it is working fine.
Here is the code :
private static final int GCM_IV_SIZE_BYTES = 12;
private static final int GCM_TAG_SIZE_BYTES = 16;
private static final int GCM_SALT_SIZE_BYTES = 16;
public static byte[] encrypt(byte[] plaintext, byte[] dataKey, String version) throws Exception
{
long startTime = System.currentTimeMillis();
// Generate Initialization Vector
byte[] IV = generateIV();
// Get Cipher Instance
Cipher cipher = getCipher();
// Get Salt
byte[] salt = generateSalt();
// Store Version
byte[] versionArr = new byte[3];
versionArr = version.getBytes();
// Generate Key
SecretKeySpec keySpec = new SecretKeySpec(dataKey, "AES");
// Create GCMParameterSpec
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_SIZE_BYTES * 8, IV);
// Initialize Cipher for ENCRYPT_MODE
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
// Perform Encryption
byte[] cipherText = cipher.doFinal(plaintext);
int capacity = 3 + GCM_SALT_SIZE_BYTES + GCM_IV_SIZE_BYTES + plaintext.length + GCM_TAG_SIZE_BYTES;
// Create ByteBuffer & add SALT, IV & CipherText
ByteBuffer buffer = ByteBuffer.allocate(capacity);
buffer.put(versionArr);
buffer.put(salt);
buffer.put(IV);
buffer.put(cipherText);
long endTime = System.currentTimeMillis();
System.out.println("Encryption Time : "+(endTime - startTime)+"ms");
// return the final encrypted cipher txt
return buffer.array();
}
public static String decrypt(byte[] cipherText, byte[] dataKey) throws Exception
{
long startTime = System.currentTimeMillis();
if (cipherText.length < GCM_IV_SIZE_BYTES + GCM_TAG_SIZE_BYTES + GCM_SALT_SIZE_BYTES) throw new IllegalArgumentException();
ByteBuffer buffer = ByteBuffer.wrap(cipherText);
byte[]version = new byte[3];
buffer.get(version, 0, version.length);
System.out.println(new String(version));
// Get Salt from Cipher
byte[] salt = new byte[GCM_SALT_SIZE_BYTES];
buffer.get(salt, 0, salt.length);
System.out.println(new String(salt));
// GET IV from cipher
byte[] ivBytes1 = new byte[GCM_IV_SIZE_BYTES];
buffer.get(ivBytes1, 0, ivBytes1.length);
System.out.println(new String(ivBytes1));
byte[] encryptedTextBytes = new byte[buffer.capacity() - salt.length - ivBytes1.length- 3];
buffer.get(encryptedTextBytes);
System.out.println("enc tect bytes");
System.out.println(new String(encryptedTextBytes));
// Get Cipher Instance
Cipher cipher = getCipher();
// Generate Key
SecretKeySpec keySpec = new SecretKeySpec(dataKey, "AES");
// Create GCMParameterSpec
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_SIZE_BYTES * 8, ivBytes1);
// Initialize Cipher for DECRYPT_MODE
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
// Perform Decryption
byte[] decryptedText = cipher.doFinal(encryptedTextBytes);
long endTime = System.currentTimeMillis();
System.out.println("Decryption Time : "+(endTime - startTime)+"ms");
return new String(decryptedText);
}
Now the issue is I have to write the same library in PHP and then I have to encrypt using PHP library and decrypt using Java library / vice-versa
Here is my PHP code for encryption:
function encrypt($key, $textToEncrypt){
$cipher = 'aes-256-gcm';
$iv_len = 12;
$tag_length = 16;
$version_length = 3;
$salt_length = 16;
$version = "v01";
$iv = openssl_random_pseudo_bytes($iv_len);
$salt = openssl_random_pseudo_bytes($salt_length);
$tag = ""; // will be filled by openssl_encrypt
$ciphertext = openssl_encrypt($textToEncrypt, $cipher, $key, 0, $iv, $tag, "", $tag_length);
$encrypted = base64_encode($version.$salt.$iv.$ciphertext.$tag);
return $encrypted;
}
Now the issue is, when I am encrypting the data using PHP and then trying to decrypt it using Java code, getting below exception
:Exception in thread "main" javax.crypto.AEADBadTagException: Tag mismatch!
at com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:578)
at com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1049)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:985)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:847)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at javax.crypto.Cipher.doFinal(Cipher.java:2164)
What am I missing here?
Base64 in PHP code is present, same is present in Java code while calling the encode/decode functions, hence not present in this post code.
The cross-platform encryption between PHP and Java using AES GCM mode is working. There are some details that may prevent you from a successful doing.
First: On PHP-side the openssl_encrypt returns a base64 encoded ciphertext that is again base64 encoded when concatenating the ciphertext with the version, iv and tag. To avoid this I set the OPENSSL-option to "OPENSSL_RAW_DATA".
Second: on Java-side the tag is appended to the ciphertext so the "ciphertext|tag" can get consumed directly.
Just a note: my examples are just showing how the encryption on PHP-side and decryption on Java-side will work but may have nothing to do with your source codes (special on Java side) - I was to lazy to adopt my example :-)
This is the output on PHP-side:
AES GCM in PHP/Java
ciphertext: djAx/kMbxfJI5Zx7lTWeDbw601cD2wkjBvuKeVBbKOZHll98GstPNfi1xHvyRlBwJDQ6YWvpymsk76kwbBbD0cBsOzzK/tH8UpA=
Copy the ciphertext to the Java program and let it run:
AES GCM in PHP/Java
decryptedtext: The quick brown fox jumps over the lazy dog
Below you find the source codes for both programs. Security warning: the codes are using fixed and hard-coded keys - don't do
this. The programs do not have any exception handling and are for educational purpose only.
The code is running on PHP > 7.2 and Java 11+.
PHP-code:
<?php
function encrypt($key, $textToEncrypt){
$cipher = 'aes-256-gcm';
$iv_len = 12;
$tag_length = 16;
$version_length = 3;
$version = "v01";
$iv = openssl_random_pseudo_bytes($iv_len);
$tag = ""; // will be filled by openssl_encrypt
$ciphertext = openssl_encrypt($textToEncrypt, $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag, "", $tag_length);
$encrypted = base64_encode($version.$iv.$ciphertext.$tag);
return $encrypted;
}
echo 'AES GCM in PHP/Java' . PHP_EOL;
// ### security warning: never use hardcoded keys in source ###
$key = '12345678901234567890123456789012';
$plaintext = 'The quick brown fox jumps over the lazy dog';
$ciphertext = encrypt($key, $plaintext);
echo 'ciphertext: ' . $ciphertext . PHP_EOL;
?>
Java-code:
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
public class SO_final {
public static void main(String[] args) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
System.out.println("AES GCM in PHP/Java");
// https://stackoverflow.com/questions/65001817/aes-gcm-in-php-java
String ciphertext = "djAx/kMbxfJI5Zx7lTWeDbw601cD2wkjBvuKeVBbKOZHll98GstPNfi1xHvyRlBwJDQ6YWvpymsk76kwbBbD0cBsOzzK/tH8UpA=";
// ### security warning: never use hardcoded keys in source ###
byte[] key = "12345678901234567890123456789012".getBytes(StandardCharsets.UTF_8);
String decryptedtext = decryptGcmBase64(key, ciphertext);
System.out.println("decryptedtext: " + decryptedtext);
}
public static String decryptGcmBase64(byte[] key, String ciphertextBase64) throws
NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException,
InvalidAlgorithmParameterException, BadPaddingException, IllegalBlockSizeException {
byte[] ciphertextComplete = Base64.getDecoder().decode(ciphertextBase64);
// split data
// base64 encoding $encrypted = base64_encode($version.$iv.$ciphertext.$tag);
byte[] version = Arrays.copyOfRange(ciphertextComplete, 0, 3); // 3 bytes
byte[] iv = Arrays.copyOfRange(ciphertextComplete, 3, 15); // 12 bytes
byte[] ciphertextWithTag = Arrays.copyOfRange(ciphertextComplete, 15, ciphertextComplete.length);
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8, iv);
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");//NOPadding
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
return new String(cipher.doFinal(ciphertextWithTag), StandardCharsets.UTF_8);
}
}

How to use ByteArrayOutputStream and ByteBuffer

I have this code in Java, how can I write it with dart?
I need to write some String to a Socket, but before it, I need to write the length of this string. So I use the ByteBuffer class to create some byte array with the string length, and after that I concatenate this two byte arrays. Thank you.
public void writeUTF(String s, OutputStream out) throws IOException {
if (s != null) {
ByteArrayOutputStream bytArray = new ByteArrayOutputStream();
int length = s.getBytes("UTF-8").length;
// Create buffer
byte[] bytesMessageLenght = ByteBuffer.allocate(4).putInt(length).array();
byte[] bytes = s.getBytes();
bytArray.write(bytesMessageLenght);
bytArray.write(bytes);
// Write
out.write(bytArray.toByteArray());
out.flush();
}
}
Use a Dart ByteData which allows you to set the length directly, and then use asUint8List to make a byte array to copy in the rest.
Uint8List encode(String s) {
var encodedString = utf8.encode(s);
var encodedLength = encodedString.length;
var data = ByteData(encodedLength + 4);
data.setUint32(0, encodedLength, Endian.big);
var bytes = data.buffer.asUint8List();
bytes.setRange(4, encodedLength + 4, encodedString);
return bytes;
}

How do you read a CertificatePolicy object from an X509 Certificate in Java?

I can use a method like this for doing it for a SKID (Subject Key Identifier). What is a comparable way to do this in JAVA, natively (not using any 3rd party libraries like bouncy castle)?
I have an x509 certificate that has this something in it and I want to be able to read this out of the certificate:
"....
[3]: ObjectId: 2.5.29.32 Criticality=false
CertificatePolicies [
[CertificatePolicyId: [5.6.7.8.9.1.2.3.4]
[] ]
]
...
"
...
import sun.security.util.DerInputStream;
import sun.security.util.DerValue;
import sun.security.x509.AuthorityKeyIdentifierExtension;
import sun.security.x509.CertificatePolicyMap;
import sun.security.x509.CertificatePolicySet;
import sun.security.x509.KeyIdentifier;
import sun.security.x509.SubjectKeyIdentifierExtension;
....
private static final String SUBJECT_KEY_ID = "2.5.29.14";
.....
/**
* Get the Subject Key Identifier of the PKI Certificate
*
* #return A String containing the SKID
* #throws CertificateParsingException
*/
public String getSubjectKeyIdentifier()
throws IllegalStateException, CertificateParsingException
{
logger.debug("entered");
// if the variable hasn't yet been initialized with a value, initialize it, else return the existing one.
if (subjectKeyIDString == null)
{
if (cert == null)
{
IllegalStateException ise = new IllegalStateException(ERROR_MSG_NOT_INITIALIZED);
logger.error(ise);
throw ise;
}
byte[] skidCertBytes = cert.getExtensionValue(SUBJECT_KEY_ID);
if (skidCertBytes == null)
{
return null;
}
String skidByteString = null;
try
{
DerValue skidDer = new DerValue(skidCertBytes);
byte[] skidDerBytes = skidDer.getDataBytes();
// create a SubjectKeyIdentifierExtension object
SubjectKeyIdentifierExtension skid = new SubjectKeyIdentifierExtension(Boolean.FALSE, skidDerBytes);
boolean isCritical = skid.isCritical();
logger.debug("isCritical: [" + isCritical + "]");
byte[] skidValueBytes = skid.getExtensionValue();
// go inside the SKID object and get the KID (KeyIdentifier) object
KeyIdentifier kid = new KeyIdentifier(skidValueBytes);
// get the bytes of the object (strips off the first two bytes, type & length bytes)
byte[] kidBytes = kid.getIdentifier();
// convert the KID bytes to a DerValue object
DerValue kidDerValue = new DerValue(kidBytes);
byte[] kidByteValue = kidDerValue.getOctetString();
// get the SKID->KID->string value
skidByteString = byteArrayToHexString(kidByteValue, true);
}
catch (IOException e)
{
logger.error(e);
}
logger.debug(skidByteString);
return skidByteString;
}
else
{
return subjectKeyIDString;
}
}

Why does Bouncy Castle RSAEngine.processblock method always returns 255 bytes when decrypting?

I am experiment with RSA using bouncy castle. I know this is the other way around to the general convention but to my understanding, it still should work theoretically.
I encrypt some data using an RSA private key. The length of the data being encrypted is 294 bytes. The encryption function outputs 512 bytes. I then call the decryption method by passing the above output cipher text and the corresponding public key. My problem is that the decryption always returns a buffer of 255 bytes whereas the actual input to the Encryption function was 294 bytes. What could be the reason for this ?
The following is the source code of the encryption and decryption functions.
public static byte[] RSAEncrypt(byte[] data, AsymmetricKeyParameter key)
{
try
{
RsaEngine e = new RsaEngine();
e.Init(true, key);
int blockSize = e.GetInputBlockSize();
List<byte> output = new List<byte>();
for (int chunkPosition = 0; chunkPosition < data.Length; chunkPosition += blockSize)
{
int chunkSize = Math.Min(blockSize, data.Length - (chunkPosition * blockSize));
output.AddRange(e.ProcessBlock(data, chunkPosition, chunkSize));
}
return output.ToArray();
}
catch (Exception ex)
{
return null;
}
}
public static byte[] RSADecrypt(byte[] data, AsymmetricKeyParameter key)
{
try
{
RsaEngine e = new RsaEngine();
e.Init(false, key);
int blockSize = e.GetInputBlockSize();
List<byte> output = new List<byte>();
for (int chunkPosition = 0; chunkPosition < data.Length; chunkPosition += blockSize)
{
int chunkSize = Math.Min(blockSize, data.Length - (chunkPosition * blockSize));
output.AddRange(e.ProcessBlock(data, chunkPosition, chunkSize));
}
return output.ToArray();
}
catch (Exception ex)
{
return null;
}
}
RSA is an asymmetric encryption method that encrypts a number less than the modulus of the RSA key (255 bytes would indicate that you're using a 256*8 = 2048 bit RSA key/modulus)
What you need to do to encrypt values greater than that is to generate a key, encrypt the data using a symmetric cipher (AES is not a bad choice) and encrypt the AES key using your private RSA key (preferably along with some other random data).
The AES key is a maximum of 256 bits, which will encrypt just fine with RSA, and AES does not have a size limit.