Send an encrypted request to APi in Dart - flutter

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.

Related

How to convert encrypt java code below in flutter

I m requirement is to convert below java encrypt code to flutter as i need to encrypt few fields value before sending to api.
Below is java encrypt code which i need to convert to java
public static String encrypt(String text, String algo, Key key) {
byte[] cipherText;
String output;
try {
if("RSA".equals(algo)) {
throw new IllegalArgumentException("Do not pass just algo pass with padding and blocking stuff!");
}
if(BuildConfig.DEBUG) {
Log.d(TAG, "encrypt in: "+text);
Log.d(TAG, "algo: "+algo);
Log.d(TAG, "key: "+key);
}
final Cipher cipher = Cipher.getInstance(algo);
if(algo.contains("AES")) {
AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
} else {
cipher.init(Cipher.ENCRYPT_MODE, key);
}
cipherText = cipher.doFinal(text.getBytes(StandardCharsets.UTF_8));
output = new String(Base64.encode(cipherText));
if(BuildConfig.DEBUG) {
Log.d(TAG, "encrypt out: "+output);
}
return output;
} catch (Exception e) {
Log.e(TAG, SDKConstants.STRING_ERROR + e, e);
return null;
}
}
char[] encPwd = Objects.requireNonNull(CryptoHelper.encrypt(Objects.requireNonNull(binding.textIPassword.getEditText()).getText().toString(), CryptoHelper.ALGO_FOR_RSA, key)).toCharArray();
Please help me in converting above java code to flutter as i need to encrypt one field before sending it to api call.
Any help is appreciated!
Just work through the Java line by line and figure out Dart equivalents. Use the other linked question as your guide. This should work (if I guessed the cipher correctly):
import 'package:pointycastle/export.dart';
String encrypt(String plainText, RSAPublicKey public) {
final plainBytes = utf8.encode(plainText) as Uint8List;
final cipher = PKCS1Encoding(RSAEngine())
..init(true, PublicKeyParameter<RSAPublicKey>(public));
final cipherBytes = cipher.process(plainBytes);
return base64Encode(cipherBytes);
}

How to encrypt a key correctly? _ FLUTTER

I am trying to implement private key encryption within my application.
However, this key that I have to encrypt is of the List type and when I try to encrypt I get the following error:
Expected a value of type 'String', but got one of type 'List <int>'
how can I encrypt my key of type List correctly without getting errors so that I can later pass the parameter as type Uint8List to my method:
AccountEntity.account (account, Uint8List.fromList (privateKey));
this is my code:
var privateKey = await account.keyPair.extractPrivateKeyBytes();
privateKey = AesEncryptionDecryption
.encryptKey(
privateKey).bytes ;
final entity =
AccountEntity.account(account, Uint8List.fromList(privateKey));
while this is the class which contains the encryption methods, as API I used encrypt: ^ 5.0.1 from pubdev :
class AesEncryptionDecryption{
static final key = encrypt.Key.fromLength(32);
static final iv = encrypt.IV.fromLength(16);
static final encrypter = encrypt.Encrypter(encrypt.AES(key));
static encryptAES(text) {
final encrypted = encrypter.encrypt(text, iv: iv);
return encrypted;
}
static encryptKey(text) {
final encrypted = encrypter.encryptBytes(text, iv: iv);
return encrypted as List<int>;
}
static String decryptAES(String base64Text) {
print(base64Text);
String decrypted = encrypter.decrypt(
Encrypted.fromBase64(base64Text),
iv: iv,
);
print(decrypted);
return decrypted;
}
does anyone know how to help me? thank you very much

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);
}
}

Decrypt AES/CBC/PKCS5Padding Encryption in Dart

I am already having encryption code in java. Now I want to consume APIs from my server. Even after trying various tutorials and sample codes I am not able to successfully decrypt the hash.
I know fixed salt and IV is not recommended at all. But for simplicity and to understand the issue I have kept salt and IV to "00000000000000000000000000000000";
Hash After Encryption from Java = "XjxCg0KK0ZDWa4XMFhykIw==";
Private key used = "Mayur12354673645"
Can someone please help me to decrypt above string using dart.
JAVA Code
public String encrypt(String salt, String iv, String passphrase,
String plaintext) {
try {
SecretKey key = generateKey(salt, passphrase);
byte[] encrypted = doFinal(Cipher.ENCRYPT_MODE, key, iv, plaintext
.getBytes("UTF-8"));
return base64(encrypted);
} catch (UnsupportedEncodingException e) {
throw fail(e);
}
}
public String decrypt(String salt, String iv, String passphrase,
String ciphertext) {
try {
SecretKey key = generateKey(salt, passphrase);
byte[] decrypted = doFinal(Cipher.DECRYPT_MODE, key, iv,
base64(ciphertext));
return new String(decrypted, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw fail(e);
}
}
private SecretKey generateKey(String salt, String passphrase) {
try {
SecretKeyFactory factory = SecretKeyFactory
.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), hex(salt),
iterationCount, keySize);
SecretKey key = new SecretKeySpec(factory.generateSecret(spec)
.getEncoded(), "AES");
return key;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private byte[] doFinal(int encryptMode, SecretKey key, String iv,
byte[] bytes) {
try {
cipher.init(encryptMode, key, new IvParameterSpec(hex(iv)));
return cipher.doFinal(bytes);
} catch (Exception e) {
e.printStackTrace();
throw fail(e);
}
}
My Dart Code
import 'package:pointycastle/block/aes_fast.dart';
import 'package:pointycastle/block/modes/cbc.dart';
import 'package:pointycastle/digests/sha1.dart';
import 'package:pointycastle/key_derivators/pbkdf2.dart';
import 'package:pointycastle/macs/hmac.dart';
import 'package:pointycastle/paddings/pkcs7.dart';
import 'package:pointycastle/pointycastle.dart';
import 'dart:convert';
import 'dart:typed_data';
import 'package:convert/convert.dart';
import 'dart:developer';
import 'package:pointycastle/random/fortuna_random.dart';
const KEY_SIZE = 16;
const ITERATION_COUNT = 5;
class EncryptionHandler {
static const CBC_MODE = 'CBC';
static Uint8List deriveKey(dynamic password,
{String salt = '0000000000000000',
int iterationCount = ITERATION_COUNT,
int derivedKeyLength = KEY_SIZE}) {
if (password == null || password.isEmpty) {
throw new ArgumentError('password must not be empty');
}
if (password is String) {
password = createUint8ListFromString(password);
}
Uint8List saltBytes = createUint8ListFromString(salt);
String hexSalt = formatBytesAsHexString(saltBytes);
KeyDerivator keyDerivator =
new PBKDF2KeyDerivator(new HMac(new SHA1Digest(), 64));
Pbkdf2Parameters params =
new Pbkdf2Parameters(saltBytes, iterationCount, derivedKeyLength);
keyDerivator.init(params);
return keyDerivator.process(password);
}
Uint8List createUint8ListFromHexString(String hex) {
var result = new Uint8List(hex.length ~/ 2);
for (var i = 0; i < hex.length; i += 2) {
var num = hex.substring(i, i + 2);
var byte = int.parse(num, radix: 16);
result[i ~/ 2] = byte;
}
return result;
}
static String formatBytesAsHexString(Uint8List bytes) {
var result = new StringBuffer();
for (var i = 0; i < bytes.lengthInBytes; i++) {
var part = bytes[i];
result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
}
return result.toString();
}
static Uint8List pad(Uint8List src, int blockSize) {
var pad = new PKCS7Padding();
pad.init(null);
int padLength = blockSize - (src.length % blockSize);
var out = new Uint8List(src.length + padLength)..setAll(0, src);
pad.addPadding(out, src.length);
return out;
}
static Uint8List unpad(Uint8List src) {
var pad = new PKCS7Padding();
pad.init(null);
int padLength = pad.padCount(src);
int len = src.length - padLength;
return new Uint8List(len)..setRange(0, len, src);
}
static String encrypt(String password, String plaintext,
{String mode = CBC_MODE}) {
Uint8List derivedKey = deriveKey(password);
KeyParameter keyParam = new KeyParameter(derivedKey);
BlockCipher aes = new AESFastEngine();
var rnd = FortunaRandom();
rnd.seed(keyParam);
Uint8List iv = createUint8ListFromString("0000000000000000");
BlockCipher cipher;
ParametersWithIV params = new ParametersWithIV(keyParam, iv);
cipher = new CBCBlockCipher(aes);
cipher.init(true, params);
Uint8List textBytes = createUint8ListFromString(plaintext);
Uint8List paddedText = pad(textBytes, aes.blockSize);
Uint8List cipherBytes = _processBlocks(cipher, paddedText);
Uint8List cipherIvBytes = new Uint8List(cipherBytes.length + iv.length)
..setAll(0, iv)
..setAll(iv.length, cipherBytes);
return base64.encode(cipherIvBytes);
}
static String decrypt(String password, String ciphertext) {
log('Password: $password');
Uint8List derivedKey = deriveKey(password);
log('derivedKey: $derivedKey');
KeyParameter keyParam = new KeyParameter(derivedKey);
log('keyParam: $keyParam');
BlockCipher aes = new AESFastEngine();
Uint8List cipherIvBytes = base64.decode(ciphertext);
log('cipherIvBytes: $cipherIvBytes');
Uint8List iv = createUint8ListFromString("0000000000000000");
// Uint8List iv = new Uint8List(aes.blockSize)
// ..setRange(0, aes.blockSize, cipherIvBytes);
log('iv: $iv');
BlockCipher cipher;
ParametersWithIV params = new ParametersWithIV(keyParam, iv);
log('params: $params');
cipher = new CBCBlockCipher(aes);
log('cipher: $cipher');
cipher.init(false, params);
int cipherLen = cipherIvBytes.length - aes.blockSize;
Uint8List cipherBytes = new Uint8List(cipherLen)
..setRange(0, cipherLen, cipherIvBytes, aes.blockSize);
Uint8List paddedText = _processBlocks(cipher, cipherBytes);
log('cipher: $paddedText');
Uint8List textBytes = paddedText;
// Uint8List textBytes = unpad(paddedText);
return new String.fromCharCodes(textBytes);
}
static Uint8List createUint8ListFromString(String s) {
var ret = new Uint8List(s.length);
for (var i = 0; i < s.length; i++) {
ret[i] = s.codeUnitAt(i);
}
return ret;
}
static Uint8List _processBlocks(BlockCipher cipher, Uint8List inp) {
var out = new Uint8List(inp.lengthInBytes);
for (var offset = 0; offset < inp.lengthInBytes;) {
var len = cipher.processBlock(inp, offset, out, offset);
offset += len;
}
return out;
}
}
The code can be simplified by using existing Dart libraries for the conversion binary to hex and vice versa. PointyCastle also supports the (PKCS7) padding, so that a custom implementation is not necessary, which also reduces the code. On the Internet you can find several dart implementations for AES/CBC/PKCS7Padding in combination with PBKDF2 that use PointyCastle, e.g. here and here.
A possible Dart implementation for decryption using the pointycastle and convert package is e.g. (for simplicity without exception handling):
import 'dart:typed_data';
import "package:pointycastle/export.dart";
import 'package:convert/convert.dart';
import 'dart:convert';
...
static Uint8List decrypt(Uint8List ciphertext, Uint8List key, Uint8List iv) {
CBCBlockCipher cipher = new CBCBlockCipher(new AESFastEngine());
ParametersWithIV<KeyParameter> params = new ParametersWithIV<KeyParameter>(new KeyParameter(key), iv);
PaddedBlockCipherParameters<ParametersWithIV<KeyParameter>, Null> paddingParams = new PaddedBlockCipherParameters<ParametersWithIV<KeyParameter>, Null>(params, null);
PaddedBlockCipherImpl paddingCipher = new PaddedBlockCipherImpl(new PKCS7Padding(), cipher);
paddingCipher.init(false, paddingParams);
return paddingCipher.process(ciphertext);
}
static Uint8List generateKey(Uint8List salt, Uint8List passphrase){
KeyDerivator derivator = new PBKDF2KeyDerivator(new HMac(new SHA1Digest(), 64));
Pbkdf2Parameters params = new Pbkdf2Parameters(salt, 5, 16);
derivator.init(params);
return derivator.process(passphrase);
}
With the posted test data:
String saltHex = '00000000000000000000000000000000';
String ivHex = '00000000000000000000000000000000';
String passphraseUtf8 = 'Mayur12354673645';
String ciphertextBase64 = "XjxCg0KK0ZDWa4XMFhykIw==";
Uint8List salt = hex.decode(saltHex);
Uint8List passphrase = utf8.encode(passphraseUtf8);
Uint8List key = generateKey(salt, passphrase);
Uint8List ciphertext = base64.decode(ciphertextBase64);
Uint8List iv = hex.decode(ivHex);
Uint8List decrypted = decrypt(ciphertext, key, iv);
print(utf8.decode(decrypted)); // This is working
the ciphertext can be decrypted to: This is working.
An alternative to PointyCastle is the cryptography package, which allows even a more compact implementation in the current case:
import 'package:cryptography/cryptography.dart';
import 'package:convert/convert.dart';
import 'dart:convert';
import 'dart:typed_data';
...
static Uint8List decrypt(Uint8List ciphertext, Uint8List key, Uint8List iv) {
SecretKey secretKey = new SecretKey(key);
Nonce nonce = new Nonce(iv);
Uint8List decrypted = aesCbc.decryptSync(ciphertext, secretKey: secretKey, nonce: nonce);
return decrypted;
}
static Uint8List generateKey(Uint8List salt, Uint8List passphrase){
Pbkdf2 pbkdf2 = Pbkdf2(macAlgorithm: new Hmac(sha1), iterations: 5, bits: 128);
return pbkdf2.deriveBitsSync(passphrase, nonce: Nonce(salt));
}
Note that in practice IV and salt must be generated randomly for each encryption (which you already mentioned in your question). Apart from that the iteration count of 5 is generally much too low.

Flutter AES128 encryption not matching with java and angular AES encryption

I have to implement a dart algorithm in order to encrypt a password and send it to a server for authentication from a flutter app (it's barely a week since I'm on flutter/dart). This has already been implemented in an android app and angular web app (not from me), which both produce the same encrypted password even though with different algorithms.
Java code for android:
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.CryptoPrimitive;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
public class AesUtil {
private final int keySize;
private final int iterationCount;
private final Cipher cipher;
public AesUtil(int keySize, int iterationCount) {
this.keySize = keySize;
this.iterationCount = iterationCount;
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw fail(e);
}
}
public String decrypt(String salt, String iv, String passphrase, String ciphertext) {
try {
SecretKey key = generateKey(salt, passphrase);
byte[] decrypted = doFinal(Cipher.DECRYPT_MODE, key, iv, base64(ciphertext));
return new String(decrypted, "UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
} catch (Exception e) {
return null;
}
}
private byte[] doFinal(int encryptMode, SecretKey key, String iv, byte[] bytes) {
try {
cipher.init(encryptMode, key, new IvParameterSpec(hex(iv)));
return cipher.doFinal(bytes);
} catch (InvalidKeyException
| InvalidAlgorithmParameterException
| IllegalBlockSizeException
| BadPaddingException e) {
return null;
}
}
private SecretKey generateKey(String salt, String passphrase) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(passphrase.toCharArray(), hex(salt), iterationCount, keySize);
SecretKey key = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
return key;
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
return null;
}
}
public static byte[] base64(String str) {
return Base64.decode(str);
}
public static String base64(byte[] bytes) {
return new String(Base64.encode(bytes));
}
public static byte[] hex(String str) {
try {
return Hex.decode(str);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public static String hex(byte[] bytes) {
return Hex.encode(bytes).toString();
}
private IllegalStateException fail(Exception e) {
return null;
}
public String random(int length) {
byte[] salt = new byte[length];
new SecureRandom().nextBytes(salt);
return hex(salt);
}
public String encrypt(String salt, String iv, String passphrase, String plaintext) {
try {
SecretKey key = generateKey(salt, passphrase);
byte[] encrypted = doFinal(Cipher.ENCRYPT_MODE, key, iv, plaintext.getBytes("UTF-8"));
return base64(encrypted);
} catch (UnsupportedEncodingException e) {
throw fail(e);
}
}
public String encriptToAES(String password) {
//`
//for some reasons key, iv and salt are hardcoded string
//
final String key = "hardcoded_key_16_characters";
final String iv = "hardcoded_iv_32_characters";
final String salt = "hardcoded_salt_32_characters";
String ciphertext = encrypt(salt, iv, key, password);
String preEncr = new String(Base64.encode((iv + "::" + salt + "::" + ciphertext).getBytes()));
return preEncr;
}
}
And this is the java line of code which generate the encrypted password:
AesUtil(128, 1000).encriptToAES(password)
JS code for angular:
import * as CryptoJS from 'crypto-js';
export class AesUtil{
private keySize;
private iterationCount;
constructor(keySize, iterationCount) {
this.keySize = keySize / 32;
this.iterationCount = iterationCount;
};
generateKey(salt, passPhrase) {
var key = CryptoJS.PBKDF2(
passPhrase,
CryptoJS.enc.Hex.parse(salt),
{ keySize: this.keySize, iterations: this.iterationCount });
return key;
}
encrypt(salt, iv, passPhrase, plainText) {
var key = this.generateKey(salt, passPhrase);
var encrypted = CryptoJS.AES.encrypt(
plainText,
key,
{ iv: CryptoJS.enc.Hex.parse(iv) });
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}
decrypt(salt, iv, passPhrase, cipherText) {
var key = this.generateKey(salt, passPhrase);
var cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: CryptoJS.enc.Base64.parse(cipherText)
});
var decrypted = CryptoJS.AES.decrypt(
cipherParams,
key,
{ iv: CryptoJS.enc.Hex.parse(iv) });
return decrypted.toString(CryptoJS.enc.Utf8);
}
}
And this code does the trick:
const iv = CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex);
const salt = CryptoJS.lib.WordArray.random(128/8).toString(CryptoJS.enc.Hex);
const aesUtil = newAesUtil(128,1000);
const cipherText = aesUtils.encrypt(salt,iv,'hardcoded_key_16_characters',password);
const aesPassword = (iv+ '::' + salt + '::'+ cipherText);
const encryptedPassword = btoa(aesPassword);
For what I can understand I need an AES128 encryption method, where to set a specific number of iterations, which accepts iv and salt and eventually encrypt the result with Base64.
I have already tried all kinds of dart encryption packages on pub.dev and snippets without success.
I was relying on this one in particular, but even with hardcoded parameters the result password doesn't match and I'm not able to figure out why. Does anyone know on which elements I have to focus in order to achieve the same result in flutter? For example, which aes128 'plugin' should I use? Static or random iv and salt? All kind of things, I'm pretty much lost. Thanks
UPDATE:
import 'dart:typed_data';
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
final password = "justatest";
final key = "0123456789abcdef";
final iv = "0123456789abcdefghijklmnopqrstuv";
final salt = "abcdefghijklmnopqrstuvwzyz012345";
encryptAESCryptoJS() {
final pbkdf2 = Pbkdf2(
macAlgorithm: Hmac(sha1),
iterations: 1000,
bits: 128,
);
final Uint8List hashBytes = pbkdf2.deriveBitsSync(
utf8.encode(key),
nonce: Nonce(utf8.encode(salt)),
);
}