I'm having some trouble to get work an AES-256-CTR encrypt/decrypt in PHP, having a previosly encrypted string made with NodeJS crypto.
Here my JS code:
var crypto = require('crypto'),
algorithm = 'aes-256-ctr',
password = 'd6F3Efeq';
function encrypt(text){
var cipher = crypto.createCipher(algorithm,password)
var crypted = cipher.update(text,'utf8','hex')
crypted += cipher.final('hex');
return crypted;
}
function decrypt(text){
var decipher = crypto.createDecipher(algorithm,password)
var dec = decipher.update(text,'hex','utf8')
dec += decipher.final('utf8');
return dec;
}
Here my PHP code:
$text = "pereira";
echo bin2hex(openssl_encrypt($text, "aes-256-ctr", "d6F3Efeq",OPENSSL_RAW_DATA));
The JS's version return this on encrypt:
148bc695286379
The PHP's version return this on my encrypt test:
2f2ad5bb09fb56
Am I missing something here? Obviosly, I neither can decrypt correctly in PHP.
Thanks in advance.
You must set iv (initialization vector) in both side.
Node JS code:
var crypto = require('crypto'),
password = '1234567890abcdef1234567890abcdef',
iv = '1234567890abcdef',
text = "pereira";
function encrypt(iv, text, password){
var cipher = crypto.createCipheriv('aes-256-ctr', password, iv)
var crypted = cipher.update(text,'utf8','hex')
crypted += cipher.final('hex');
return crypted;
}
function decrypt(iv, text, password){
var decipher = crypto.createDecipheriv('aes-256-ctr', password, iv)
var dec = decipher.update(text,'hex','utf8')
dec += decipher.final('utf8');
return dec;
}
console.log(encrypt(iv, text, password));
And PHP code:
$text = 'pereira';
$algorithm = 'aes-256-ctr';
$password = '1234567890abcdef1234567890abcdef'; //node js required 32 byte length key
$iv = '1234567890abcdef'; //must be 16 byte length
echo bin2hex(openssl_encrypt($text, $algorithm, $password, OPENSSL_RAW_DATA, $iv));
Finally I got a solution for my question, thanks to #Alex K and #Artjom B.
As I mention in a comment, I must get a PHP encrypt/descrypt compatible with the NodeJS createCipher because the NodeJS application is a thrid party one.
So, the JS code is like this:
var crypto = require('crypto'),
algorithm = 'aes-256-ctr',
password = 'd6F3Efeq';
function encrypt(text){
var cipher = crypto.createCipher(algorithm,password)
var crypted = cipher.update(text,'utf8','hex')
crypted += cipher.final('hex');
return crypted;
}
function decrypt(text){
var decipher = crypto.createDecipher(algorithm,password)
var dec = decipher.update(text,'hex','utf8')
dec += decipher.final('utf8');
return dec;
}
I wrote a little PHP class to achieve this encrypt/decrypt job (Based on #Artjon's solution):
class Crypto
{
const METHOD = 'aes-256-ctr';
public function encrypt($plaintext, $password, $salt='', $encode = false)
{
$keyAndIV = self::evpKDF($password, $salt);
$ciphertext = openssl_encrypt(
$plaintext,
self::METHOD,
$keyAndIV["key"],
OPENSSL_RAW_DATA,
$keyAndIV["iv"]
);
$ciphertext = bin2hex($ciphertext);
if ($encode)
{
$ciphertext = base64_encode($ciphertext);
}
return $ciphertext;
}
public function decrypt($ciphertext, $password, $salt='', $encoded = false)
{
if ( $encoded )
{
$ciphertext = base64_decode($ciphertext, true);
if ($ciphertext === false)
{
throw new Exception('Encryption failure');
}
}
$ciphertext = hex2bin($ciphertext);
$keyAndIV = self::evpKDF($password, $salt);
$plaintext = openssl_decrypt(
$ciphertext,
self::METHOD,
$keyAndIV["key"],
OPENSSL_RAW_DATA,
$keyAndIV["iv"]
);
return $plaintext;
}
public function evpKDF($password, $salt, $keySize = 8, $ivSize = 4, $iterations = 1, $hashAlgorithm = "md5")
{
$targetKeySize = $keySize + $ivSize;
$derivedBytes = "";
$numberOfDerivedWords = 0;
$block = NULL;
$hasher = hash_init($hashAlgorithm);
while ($numberOfDerivedWords < $targetKeySize)
{
if ($block != NULL)
{
hash_update($hasher, $block);
}
hash_update($hasher, $password);
hash_update($hasher, $salt);
$block = hash_final($hasher, TRUE);
$hasher = hash_init($hashAlgorithm);
// Iterations
for ($i = 1; $i < $iterations; $i++)
{
hash_update($hasher, $block);
$block = hash_final($hasher, TRUE);
$hasher = hash_init($hashAlgorithm);
}
$derivedBytes .= substr($block, 0, min(strlen($block), ($targetKeySize - $numberOfDerivedWords) * 4));
$numberOfDerivedWords += strlen($block)/4;
}
return array(
"key" => substr($derivedBytes, 0, $keySize * 4),
"iv" => substr($derivedBytes, $keySize * 4, $ivSize * 4)
);
}
}
According to the NodeJS documentation, It derives the IV from the password with no salt, so I used an empty string as salt.
I'm using this PHP class in CodeIgniter like this:
$this->load->library("Crypto");
$plain_text = "pereira";
$password = "d6F3Efeq";
$enc_text = $this->crypto->encrypt($plain_text,$password);
$pla_text = $this->crypto->decrypt($enc_text,$password);
echo $enc_text."<br>";
echo $pla_text;
Both of them (NodeJS and PHP) return the same result for encrypt and decrypt function:
pereira = 148bc695286379
Regards.
Related
I have a PHP aes256 decrypt function I am trying to convert to typescript in ionic 5 but I am faced with the following error using md5:
This expression is not callable. Type 'any[]' has no call
signatures.
PHP decrypt function:
function decrypt(string $jsonStr, string $passphrase)
{
$json = json_decode($jsonStr, true);
$salt = hex2bin($json["s"]);
$iv = hex2bin($json["iv"]);
$ct = base64_decode($json["ct"]);
$concatedPassphrase = $passphrase . $salt;
$md5 = [];
$md5[0] = md5($concatedPassphrase, true);
$result = $md5[0];
for ($i = 1; $i < 3; $i++) {
$md5[$i] = md5($md5[$i - 1] . $concatedPassphrase, true);
$result .= $md5[$i];
}
$key = substr($result, 0, 32);
$data = openssl_decrypt($ct, 'aes-256-cbc', $key, true, $iv);
return json_decode($data, true);
}
Typescript converted:
async dcrypt(data: string, passphrase: string){
const json = JSON.parse(data);
const salt = decode(json["s"]);
const iv = decode(json["iv"]);
const ct = decode(json["ct"]);
const concatedPassphrase = passphrase + salt;
const md5 = [];
md5[0] = md5(concatedPassphrase, true);
let result = md5[0];
for (let i = 1; i < 3; i++) {
md5[i] = md5(md5[i - 1] + concatedPassphrase, true);
result += md5[i];
}
const key = result.substr(0, 32);
const importedKey = await window.crypto.subtle.importKey(
'raw',
new Uint8Array(key),
{
name: 'AES-CBC'
},
false,
['decrypt']
);
const data = await window.crypto.subtle.decrypt(
{
name: 'AES-CBC',
iv: new Uint8Array(iv)
},
importedKey,
new Uint8Array(ct)
);
return JSON.parse(new TextDecoder().decode(data));
}
Both md5 functions: md5(concatedPassphrase, true) && md5(md5[i - 1] + concatedPassphrase, true) are giving me the following error:
This expression is not callable. Type 'any[]' has no call
signatures.
How can I resolve this issue using md5?
I am trying to convert some existing php code into nodejs but node js code returns:
TypeError: Salt must be a buffer
I am using node version => v8.11.2
PHP Code :
class SecurityModel {
protected $key;
protected $method;
protected $data;
protected $iv;
function __construct($data, $key = 'testing', $method = 'AES-256-CBC',$InitialVector = "aw90rela942f65u2") {
$this->data = $data;
$this->key = $this->passwordDeriveBytes($key, null);
$this->method = $method;
$this->iv = $InitialVector;
}
function passwordDeriveBytes($password, $salt, $iterations = 100, $len = 32) {
$key = $password . $salt;
for($i = 0; $i < $iterations; $i++) {
$key = sha1($key, true);
}
if (strlen($key) < $len) {
$hx = $this->passwordDeriveBytes($password, $salt, $iterations - 1, 20);
$counter = 0;
while (strlen($key) < $len) {
$counter += 1;
$key .= sha1($counter . $hx, true);
}
}
return substr($key, 0, $len);
}
function encrypt(): string {
return openssl_encrypt($this->data, "aes-256-cbc", $this->key, 0, $this->iv);
}
function decrypt(): string {
return openssl_decrypt($this->data, "aes-256-cbc", $this->key, 0, $this->iv);
}
}
$objSecurityModel = new SecurityModel('437217');
$Encrypted = $objSecurityModel->encrypt();
echo "Encrypted :".$Encrypted ."<br>"; //returns-->C9xJGa03dRQx9ePm0nLnHg==
$objSecurityModel = new SecurityModel($Encrypted);
echo "Decrypted::".$objSecurityModel->decrypt(); //returns-->437217
I tried some what in nodejs
NodeJs Code :
const express = require('express');
const app = express();
var crypto = require('crypto');
key = 'testing'
plaintext = '437217'
iv = 'aw90rela942f65u2'
crypto.pbkdf2('testing', null, 100, 32, 'AES-256-CBC', (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey.toString('hex')); // '3745e48...08d59ae'
key = derivedKey.toString('hex');
});
cipher = crypto.createCipheriv('aes-256-cbc', key,iv)
decipher = crypto.createDecipheriv('aes-256-cbc', key,iv);
var encryptedPassword = cipher.update(plaintext, 'utf8', 'base64');
encryptedPassword += cipher.final('base64')
var decryptedPassword = decipher.update(encryptedPassword, 'base64', 'utf8');
decryptedPassword += decipher.final('utf8');
console.log('original :', plaintext);
console.log('encrypted :', encryptedPassword);
console.log('decrypted :', decryptedPassword);
//PORT
const port = process.env.PORT || 3000;
app.listen(port,() => console.log(`Listening on port ${port}....`));
PBKDF2 is a great idea and is what the PHP code should have done in the first place. Unfortunately what happens inside passwordDeriveBytes() is nowhere near PBKDF2. You need to reproduce the looping like what happens inside passwordDeriveBytes() if you want to match it.
Oh and "Salt must be a buffer" is solved by converting the IV to a Buffer with Buffer.from(iv) (that is also a sign that a good IV should not be a string but random bytes).
const crypto = require('crypto');
function sha1(input) {
return crypto.createHash('sha1').update(input).digest();
}
function passwordDeriveBytes(password, salt, iterations, len) {
var key = Buffer.from(password + salt);
for(var i = 0; i < iterations; i++) {
key = sha1(key);
}
if (key.length < len) {
var hx = passwordDeriveBytes(password, salt, iterations - 1, 20);
for (var counter = 1; key.length < len; ++counter) {
key = Buffer.concat([key, sha1(Buffer.concat([Buffer.from(counter.toString()), hx]))]);
}
}
return Buffer.alloc(len, key);
}
var password = 'testing';
var plaintext = '437217';
var iv = 'aw90rela942f65u2';
//var key = crypto.pbkdf2Sync(password, '', 100, 32, 'sha1'); // How it should be
var key = passwordDeriveBytes(password, '', 100, 32); // How it is
console.log(key.toString('hex'));
var cipher = crypto.createCipheriv('aes-256-cbc', key, Buffer.from(iv));
var decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.from(iv));
var part1 = cipher.update(plaintext, 'utf8');
var part2 = cipher.final();
var encrypted = Buffer.concat([part1, part2]).toString('base64');
var decrypted = decipher.update(encrypted, 'base64', 'utf8');
decrypted += decipher.final();
console.log('original :', plaintext);
console.log('encrypted :', encrypted);
console.log('decrypted :', decrypted);
Output:
df07df624db35d0bcf5fe7ff2dfdfffcef93f098939d750ca55595ae1b33925d
original : 437217
encrypted : C9xJGa03dRQx9ePm0nLnHg==
decrypted : 437217
How can I send the data of a SecretKeySpec or SecretKey from Java to PHP? When I convert it to String (to encrypt with RSA and post to PHP), I get some diamond with a question mark in it.
I tried to use utf8_decode but this problem is still remains.
EDIT:
I generated RSA Keypair in PHP, and sent the Public Key to Java. In Java I want to generate an AES_Key and encrypt a plaintext using AES, and send an encrypted plaintext and AES_Key (that is encrypted with RSA_Public_Key) from Java to PHP.
The following codes represent the AES Encryption/Decryption steps of this project:
Java:
public static class ApiCrypter {
private String iv = "0000000000000000";
private byte[] secretkey; //="mysecretkeyyyyyy";
private IvParameterSpec ivspec;
private SecretKeySpec keyspec;
private Cipher cipher;
public ApiCrypter(byte[] key)
{
secretkey=key;
ivspec = new IvParameterSpec(iv.getBytes());
keyspec = new SecretKeySpec(secretkey, "AES");
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
}
}
public byte[] encrypt(String text) throws Exception
{
if(text == null || text.length() == 0) {
throw new Exception("Empty string");
}
byte[] encrypted = null;
try {
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
encrypted = cipher.doFinal(text.getBytes("UTF-8"));
}
catch (Exception e) {
throw new Exception("[encrypt] " + e.getMessage());
}
return encrypted;
}
public byte[] decrypt(String code) throws Exception
{
if(code == null || code.length() == 0) {
throw new Exception("Empty string");
}
byte[] decrypted = null;
try {
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
decrypted = cipher.doFinal(hexToBytes(code));
}
catch (Exception e) {
throw new Exception("[decrypt] " + e.getMessage());
}
return decrypted;
}
public static String bytesToHex(byte[] data)
{
if (data==null) {
return null;
}
int len = data.length;
String str = "";
for (int i=0; i<len; i++) {
if ((data[i]&0xFF)<16) {
str = str + "0" + java.lang.Integer.toHexString(data[i]&0xFF);
}
else {
str = str + java.lang.Integer.toHexString(data[i]&0xFF);
}
}
return str;
}
public static byte[] hexToBytes(String str) {
if (str==null) {
return null;
}
else if (str.length() < 2) {
return null;
}
else {
int len = str.length() / 2;
byte[] buffer = new byte[len];
for (int i=0; i<len; i++) {
buffer[i] = (byte) Integer.parseInt(str.substring(i*2,i*2+2),16);
}
return buffer;
}
}
}
PHP:
<?php
class ApiCrypter
{
private $iv = '0000000000000000';
private $key;// = '89432hjfsd891787';
public function __construct($keyy)
{
$this->key = $keyy;
}
public function encrypt($str)
{
$encrypted = openssl_encrypt($str, 'AES-128-CBC', $this->key, 0, $this->iv);//
return bin2hex($encrypted);
}
public function decrypt($code)
{
$code = $this->hex2bin($code);
$decrypted = openssl_decrypt($code, 'AES-128-CBC', $this->key, 0, $this->iv);
$ut = utf8_encode(trim($decrypted));
// return $this->pkcs5_unpad($ut);
return $ut;
}
protected function hex2bin($hexdata)
{
$bindata = '';
for ($i = 0; $i < strlen($hexdata); $i += 2) {
$bindata .= chr(hexdec(substr($hexdata, $i, 2)));
}
return $bindata;
}
protected function pkcs5_pad($text)
{
$blocksize = 16;
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}
protected function pkcs5_unpad($text)
{
$pad = ord($text{strlen($text) - 1});
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}
}
?>
Generating a secretKey (as AES key), encryption of the plaintext(name) by AES key, Encrypt AES key, and preparing them for sending to PHP:
SecureRandom secureRandom = new SecureRandom();
KeyGenerator keyGenerator;
keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256, secureRandom);
SecretKey key = keyGenerator.generateKey();
secret_key = key.getEncoded();
Byte[] encodedSessionKey = RSA.encryptByPublicKey(secret_key, public_key);
ApiCrypter apiCrypter=new ApiCrypter(encodedSessionKey);
String encryptedName = ApiCrypter.bytesToHex(apiCrypter.encrypt(nameStr));
String encodedStr = Base64.encodeToString(encodedSessionKey, Base64.DEFAULT);
AsyncDataClass asyncRequestObject = new AsyncDataClass();
asyncRequestObject.execute(serverUrl,encodedStr,encryptedName);
PHP code to decrypt AES key using of the RSA private key and decryption of the NameString by decrypted AES key:
$encodedStr = "";
$encryptedName = "";
if(isset($_POST['encodedStr'])){
$encodedSecretKey = $_POST['encodedStr'];
}
if(isset($_POST['encryptedName'])){
$encryptedName = $_POST['encryptedName'];
}
$rsa = new \phpseclib\Crypt\RSA();
$rsa->setHash('sha1');
$rsa->setMGFHash('sha1');
$rsa->setEncryptionMode(1);
$rsa->loadKey(file_get_contents('private.key')); // RSA Private key
$AES_session_key = $rsa->decrypt(base64_decode($encodedSecretKey));
$ApiCrypter= new ApiCrypter($AES_session_key);
$decryptedName = $ApiCrypter->decrypt($encryptedName);
Are these classes match to each other?
How to encrypt a plaintext in Java using of the AES_Key (or a copy of a SecretKey that is generated in Java)?
How to decrypt a plaintext in PHP, using the AES_key(that is decrypted by RSA Private Key) ?
A secret key consists of random bytes. That means that - whatever encoding you use - that some bytes won't print. To represent bytes as text usually you use hexadecimal or base64 encoding. The difference between those two is that a normal person can make out the byte values from hexadecimals while base64 is more efficient (as it uses more characters and therefore can encode 6 bits per character where hexadecimals only encodes 4 bits per character).
Within Java 8 only base 64 is supported by default:
System.out.println(Base64.getEncoder().encodeToString(secretKey.getEncoded()));
and for Android:
Base64.encodeToString(secretKey.getEncoded(),Base64.DEFAULT);
should work fine (for SecretKeySpec as well, as it implements SecretKey).
Of course sending an unprotected secret key should be performed in a secure environment and should in general be avoided at all costs.
Here's some C# code (I've modified it slightly to modify some of the hard coded values in it):
public static string Decrypt(string InputFile)
{
string outstr = null;
if ((InputFile != null))
{
if (File.Exists(InputFile))
{
FileStream fsIn = null;
CryptoStream cstream = null;
try
{
byte[] _b = { 94, 120, 102, 204, 199, 246, 243, 104, 185, 115, 76, 48, 220, 182, 112, 101 };
fsIn = File.Open(InputFile, FileMode.Open, System.IO.FileAccess.Read);
SymmetricAlgorithm symm = new RijndaelManaged();
PasswordDeriveBytes Key = new PasswordDeriveBytes(System.Environment.MachineName, System.Text.Encoding.Default.GetBytes("G:MFX62rlABW:IUYAX(i"));
ICryptoTransform transform = symm.CreateDecryptor(Key.GetBytes(24), _b);
cstream = new CryptoStream(fsIn, transform, CryptoStreamMode.Read);
StreamReader sr = new StreamReader(cstream);
char[] buff = new char[1000];
sr.Read(buff, 0, 1000);
outstr = new string(buff);
}
finally
{
if (cstream != null)
{
cstream.Close();
}
if (fsIn != null)
{
fsIn.Close();
}
}
}
}
return outstr;
}
I need to come up with a function to do the same in PHP. Bear in mind, I did not write the C# code and I cannot modify it, so even if it's bad, I'm stuck with it. I've searched all over and have found bits and pieces around, but nothing that works so far. All examples I've found use mcrypt, which seems to be frowned upon these days, but I'm probably stuck using it. Next, I found the following post which has some useful info: Rewrite Rijndael 256 C# Encryption Code in PHP
So looks like the PasswordDeriveBytes class is the key to this. I created the following PHP code to try to decrypt:
function PBKDF1($pass,$salt,$count,$dklen) {
$t = $pass.$salt;
//echo 'S||P: '.bin2hex($t).'<br/>';
$t = sha1($t, true);
//echo 'T1:' . bin2hex($t) . '<br/>';
for($i=2; $i <= $count; $i++) {
$t = sha1($t, true);
//echo 'T'.$i.':' . bin2hex($t) . '<br/>';
}
$t = substr($t,0,$dklen);
return $t;
}
$input = 'Ry5WdjGS8rpA9eA+iQ3aPw==';
$key = "win7x64";
$salt = implode(unpack('C*', "G:MFX62rlABW:IUYAX(i"));
$salt = pack("H*", $salt);
$it = 1000;
$keyLen = 16;
$key = PBKDF1($key, $salt, $it, $keyLen);
$key = bin2hex(substr($key, 0, 8));
$iv = bin2hex(substr($key, 8, 8));
echo trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, base64_decode($input), MCRYPT_MODE_CBC, $iv));
You'll note that for what I believe to be System.Environment.MachineName, I put in a fixed value for now which is the computer name of the machine I'm on, so should be equivalent of what the C# code is doing. Other than that, I've noticed that using MCRYPT_RIJNDAEL_256 doesn't work, it throws the error "The IV parameter must be as long as the blocksize". If I use MCRYPT_RIJNDAEL_128, I don't get that error, but decryption still fails. I assume I'm missing the piece for the byte array _b that's used by the CreateDecryptor function, I have no idea where that's supposed to fit in. Any help is appreciated.
UPDATE
This is the solution, which was made possible by the answer marked correct. Note that the code for the PBKDF1 function is not mine, it was linked to in the answer.
function PBKDF1($pass, $salt, $count, $cb) {
static $base;
static $extra;
static $extracount= 0;
static $hashno;
static $state = 0;
if ($state == 0)
{
$hashno = 0;
$state = 1;
$key = $pass . $salt;
$base = sha1($key, true);
for($i = 2; $i < $count; $i++)
{
$base = sha1($base, true);
}
}
$result = "";
if ($extracount > 0)
{
$rlen = strlen($extra) - $extracount;
if ($rlen >= $cb)
{
$result = substr($extra, $extracount, $cb);
if ($rlen > $cb)
{
$extracount += $cb;
}
else
{
$extra = null;
$extracount = 0;
}
return $result;
}
$result = substr($extra, $rlen, $rlen);
}
$current = "";
$clen = 0;
$remain = $cb - strlen($result);
while ($remain > $clen)
{
if ($hashno == 0)
{
$current = sha1($base, true);
}
else if ($hashno < 1000)
{
$n = sprintf("%d", $hashno);
$tmp = $n . $base;
$current .= sha1($tmp, true);
}
$hashno++;
$clen = strlen($current);
}
// $current now holds at least as many bytes as we need
$result .= substr($current, 0, $remain);
// Save any left over bytes for any future requests
if ($clen > $remain)
{
$extra = $current;
$extracount = $remain;
}
return $result;
}
$input = 'base 64 encoded string to decrypt here';
$key = strtoupper(gethostname());
$salt = 'G:MFX62rlABW:IUYAX(i';
$it = 100;
$keyLen = 24;
$key = PBKDF1($key, $salt, $it, $keyLen);
$iv = implode(array_map('chr', [94, 120, 102, 204, 199, 246, 243, 104, 185, 115, 76, 48, 220, 182, 112, 101]));
_b is a static value that is used as the IV (CreateDecryptor takes a key and IV parameter). Since it is 16 bytes long, this means that you're using Rijndael-128 or more commonly known AES.
Key.GetBytes(24) suggests that a 24 byte key is derived and not a 16 byte key.
Make sure that
System.Text.Encoding.Default is equivalent with implode(unpack('C*', ...,
Default value for iterations of PasswordDeriveBytes is 1000,
Default value for hash of PasswordDeriveBytes is SHA-1
Security problems:
PBKDF1 is obsolete and PBKDF2 isn't that much better. Use up-to-date key derivation algorithms like Argon2 or scrypt.
The IV must be randomly chosen to achieve semantic security. It doesn't have to be secret, so it can be sent along with the ciphertext.
Stretching a key by encoding it to hex doesn't provide any security (don't use bin2hex).
The ciphertext is not authenticated, which means that you cannot detect (malicious) manipulation of encrypted messages. Employ encrypt-then-MAC.
A guy told me to encrypt data by using a dynamic key generated according to the following algorithm
timestamp = '080717032817'
static_key = A270AE59FF7782A2EDFE1A781BAB888D0B749D265865C288
k1 = first 5 bytes of the static key
k2 = first 3 bytes of the integer value of the timestamp
k3 = last 5 bytes of the static key
k4 = last 5 bytes of the timestamp
dynamic_key = k1k2k3k4
He told be that the data should be padded :
pad to reach a multiple of the blocksize (8byte for 3DES CBC),
or
pad with 8 null bytes if the length of the data is already multiple of 8.
In his example, with an iv='0123456789ABCDEF' he obtains:
<DATA> <TIMESTAMP> <BASE64 CRYPTED DATA>
3408682266,080717032817,hkUIYwssDQCq0GLx/5MiZg==
To implement the algo, I wrote this class functions
private function __encrypt($data,$parameters,$time,$convert = false)
{
$staticKey =
$mode = MCRYPT_MODE_CBC;
$ivi = '0123456789ABCDEF';
$cipher = MCRYPT_3DES;
$this->log[] = "Encrypt params: mode => {$mode}, cipher => {$cipher}, iv =>{$ivi}";
$dynamicKey =
$iv = pack('H*', $ivi);
$this->log[] = 'Initial Vector '. var_dump($iv);
$data = $this->__padder($data,mcrypt_get_block_size($cipher, $mode),$convert);
$this->log[] = ('Data After padding: ' . $data .", length (bytes):" . strlen($data)/2);
try {
$output = mcrypt_encrypt($cipher, $dynamicKey, $data, $mode,$iv);
} catch (Exception $ex) {
debug($ex->getMessage());
throw new Exception($ex->getMessage());
}
return $output;
}
/**
* Generate a dynamic key based on a timestamp and a static key
* #param type $static_key
*/
private function __generateDynamicKey($static_key = '', $time)
{
$dateObj = DateTime::createFromFormat("ymdHis", $time);
$k[1] = substr($static_key,0, 10);
$k[2] = substr($time,0,6);debug($k[2]);
$k[3] = substr($static_key,strlen($static_key) - 10, 10);
$k[4] = substr($time,strlen($time)-6,6);debug($k[4]); //last 3 bytes
$this->log[] = ("Dynamic key =>".join("",$k). ', length in bytes=>'. strlen(pack('H*', join("",$k))));
return pack('H*', join("",$k));
}
/**
*
* #param type $data
* #param type $blockSize
* #param type $convert
* #return string
*/
private function __padder($data,$blockSize = 8,$convert = false)
{
if ($convert)
$data = Generic::strToHex($data); // hex representation of the data
$this->log[] = 'Block size of cipher: ' .$blockSize;
$this->log[] = ("Hex value before padding=>".($data) );
//Chek if the data is padded to 16 bytes
$dataBytes = strlen($data) / 2 ; // 1 byte = 2 Hex digits
$this->log[] = "Data num. of bytes " . $dataBytes;
$rest = $dataBytes % $blockSize; // The num of bytes is a multiple of blockSize ?
$nearest = ceil($dataBytes/$blockSize ) * $blockSize;
$output = $data;
if ($rest != 0)
{
$delta = ($nearest - $dataBytes); // in bytes
$deltaValue = Generic::zeropad($delta, 2);
$this->log[] = ('padding value '.$deltaValue);
}
else
{
$this->log[] = ('Add 8 bytes of padding!');
$delta = 8;
$deltaValue = '00';
}
$output = $data . str_repeat($deltaValue, $delta);
$this->log[] = ('Hex value after padding '. $output . ', length in bytes =>' . strlen($output)/2);
return $output;
}
public function test($clearUserCode)
{
$userCode = $this->__encrypt($clearUserCode,$provider,$time,true); //UserCode is given as string, mut be converted in hex
$this->log[] = ('UserCode Clear : ' . $clearUserCode . ', in hex: ' . Generic::strToHex($clearUserCode));
$this->log[] = ('UserCode Crypt : ' . bin2hex($userCode));
$this->log[] = ('UserCode Crypt and base64: ' . base64_encode(($userCode)));
$this->log[] = ('----------------------End encrypt UserCode part----------------------') ;
}
And finally somewhere
$this->test('3408682266');
which give to me a different result:
UserCode Clear : 3408682266, in hex: 33343038363832323636
UserCode Crypt : 9d7e195a8d85aa7d051362dfae0042c2
UserCode Crypt and base64: nX4ZWo2Fqn0FE2LfrgBCwg==
Any hint?
After googling somewhere, I discovered that 3DES wants a 192 bits key, and in some way php's mcrypt does not do that magically: You have to pad the key yourself! These are the two functions that work for the examples of the question:
/**
* Make ciphering (3DES) in pkc7 (padding with 0 or a filling value)
* key must bey 24 bytes (192 bits)
* #param type $key
* #param type $iv
* #param type $text
* #return type
*/
public static function encryptNET3DES($key,$iv,$text)
{
$td = mcrypt_module_open(MCRYPT_3DES, '', MCRYPT_MODE_CBC, '');
// Complete the key
$key_add = 24-strlen($key);
$key .= substr($key,0,$key_add);
// Padding the text
$block = mcrypt_get_block_size("tripledes", "cbc");
$len = strlen($text);
$padding = $block - ($len % $block);
$text .= str_repeat(chr($padding),$padding);
mcrypt_generic_init ($td, $key, $iv);
$encrypt_text = mcrypt_generic ($td, $text);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $encrypt_text;
}
/**
* Decrypt 3DES encrypted data with 24-bit key
* #param type $key
* #param type $iv
* #param type $text
* #return type
*/
public static function decryptNET3DES($key,$iv,$text)
{
$td = mcrypt_module_open (MCRYPT_3DES, "", MCRYPT_MODE_CBC, "");
// Complete the key
$key_add = 24-strlen($key);
$key .= substr($key,0,$key_add);
mcrypt_generic_init ($td, $key, $iv);
$decrypt_text = mdecrypt_generic ($td, $text);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
//remove the padding text
$block = mcrypt_get_block_size("tripledes", "cbc");
$packing = ord($decrypt_text{strlen($decrypt_text) - 1});
if($packing && ($packing < $block))
// Get rid of padded data
{
for($P = strlen($decrypt_text) - 1; $P >= strlen($decrypt_text) - $packing; $P--) {
if(ord($decrypt_text{$P}) != $packing)
{
$packing = 0;
}
}
}
$decrypt_text = substr($decrypt_text,0,strlen($decrypt_text) - $packing);
return $decrypt_text;
}