0

I have managed to get it working with text not containing chars outside og a-zA-Z0-9 and some special chars, but if i use danish letters like æøåÆØÅ, the decrypted text shows ? instead of the actual letter. All files are saved as UTF-8, and headers charset=utf-8

Javascript --- input: "tester for æøåÆØÅ#¤%&/"

var arrayBufferToString=function(buffer){
  var str = "";
  for (var iii = 0; iii < buffer.byteLength; iii++){
    str += String.fromCharCode(buffer[iii]);
  }
  return str;
}
var stringToArrayBuffer=function(str){
  var bytes = new Uint8Array(str.length);
  for (var iii = 0; iii < str.length; iii++){
    bytes[iii] = str.charCodeAt(iii);
  }
  return bytes;
}
var arrayBufferToHexDec=function(buffer) {
  return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}

var pwVector,key,sendData={};
var pwGenerateSymmetricKey=function(pw){
  if(this.crypto){
    if(!pwVector)pwVector = crypto.getRandomValues(new Uint8Array(16));
    crypto.subtle.digest({name: "SHA-256"}, stringToArrayBuffer(pw)).then(function(result){
      window.crypto.subtle.importKey("raw", result, {name: "AES-CBC",length:256}, false, ["encrypt", "decrypt"]).then(function(e){

        console.log(e+" gen SHA256 key: "+arrayBufferToHexDec(result));

        sendData.pwVector=btoa(arrayBufferToHexDec(pwVector.buffer));
        sendData.pwKey=btoa(arrayBufferToHexDec(result));

        sendData.pwVectorString=btoa(arrayBufferToString(pwVector));
        sendData.pwKeyString=btoa(arrayBufferToString(new Uint8Array(result)));

        key = e;
      },
      function(e){
        console.log(e);
      });
    });
  }else return;
} 
var pwEncryptData=function(input){
  crypto.subtle.encrypt({name: "AES-CBC", iv: this.pwVector},key,stringToArrayBuffer(input)).then(
    function(result){
      console.log('encrypted',result)

      var pwEncryptedDataArray=new Uint8Array(result);
      var pwEncryptedDataString=arrayBufferToString(pwEncryptedDataArray);

      sendData.pwData=arrayBufferToHexDec(result);
      sendData.pwDataString=btoa(pwEncryptedDataString);
    }, 
    function(e){
      console.log(e.message);
    }
  );
}       

Php:

function strToHex($string){
    $hex = '';
    for ($i=0; $i<strlen($string); $i++){
        $ord = ord($string[$i]);
        $hexCode = dechex($ord);
        $hex .= substr('0'.$hexCode, -2);
    }
    return $hex;
}

$json_string = file_get_contents('php://input');
$json_object = json_decode($json_string);

$oEncryptedData=hex2bin($json_object->pwData);
$sEncryptedData=hex2bin(strToHex(base64_decode($json_object->pwDataString)));

$aesKey=hex2bin(base64_decode($json_object->pwKey));
$iv=hex2bin(base64_decode($json_object->pwVector));

$aesKeyStr=hex2bin(strToHex(base64_decode($json_object->pwKeyString)));
$ivStr=hex2bin(strToHex(base64_decode($json_object->pwVectorString)));

$oDecryptedData = decrypt($aesKey,$iv,$oEncryptedData);
$sDecryptedData = decrypt($aesKeyStr,$ivStr,$sEncryptedData);

function decrypt($aesKey,$iv, $dataTodecrypt) {
    $output = false;
    $output = openssl_decrypt($dataTodecrypt, 'AES-256-CBC',$aesKey,OPENSSL_RAW_DATA, $iv);
    return $output;
}
echo $oDecryptedData; // output: tester for ??????#?%&/
echo $sDecryptedData; // output: tester for ??????#?%&/

I have tried with options 0, OPENSSL_ZERO_PADDING and OPENSSL_RAW_DATA, with same result. Can anybody help me, because i am stuck right now, and cant find the possible error.

Solution

The problem is solved if the input string is encoded to utf-8 before encryption in Javascript, by chancing this line:

  crypto.subtle.encrypt({name: "AES-CBC", iv: this.pwVector},key,stringToArrayBuffer(input)).then(

To this:

  crypto.subtle.encrypt({name: "AES-CBC", iv: this.pwVector},key,stringToArrayBuffer(unescape(encodeURI(input)))).then(

Pretty simple. Seems strange that the input from HTML is not utf-8 encoded even though

<meta charset="UTF-8">

is set. But there it is :-)

gerteb
  • 121
  • 1
  • 12

1 Answers1

3

Javascript side would look like this:

function stringToArrayBuffer(str) {
    var buf = new ArrayBuffer(str.length);
    var bufView = new Uint8Array(buf);
    for (var i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

function arrayBufferToString(str) {
    var byteArray = new Uint8Array(str);
    var byteString = '';
    for (var i = 0; i < byteArray.byteLength; i++) {
        byteString += String.fromCodePoint(byteArray[i]);
    }
    return byteString;
}

var text = "tester for æøåÆØÅ#¤%&/";
console.log("what we will encrypt:");
console.log(text);

var data = stringToArrayBuffer(text);
console.log("what we encode to:");
console.log(data);

window.crypto.subtle.generateKey({
            name: "AES-CBC",
            length: 256, //can be  128, 192, or 256
        },
        false, //whether the key is extractable (i.e. can be used in exportKey)
        ["encrypt", "decrypt"] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey"
    )
    .then(function(key) {
        //returns a key object
        console.log("our key:");
        console.log(key);

        window.crypto.subtle.encrypt({
                    name: "AES-CBC",
                    //Don't re-use initialization vectors!
                    //Always generate a new iv every time your encrypt!
                    iv: window.crypto.getRandomValues(new Uint8Array(16)),
                },
                key, //from generateKey or importKey above
                data //ArrayBuffer of data you want to encrypt
            )
            .then(function(encrypted) {
                //returns an ArrayBuffer containing the encrypted data
                console.log("our ciphertext:");
                console.log(new Uint8Array(encrypted));
            })
            .catch(function(err) {
                console.error(err);
            });
    })
    .catch(function(err) {
        console.error(err);
    });

As long as the string is encoded as an array, should just need to turn the array into a string on the other side:

$decrypted = openssl_decrypt(
    $EncryptedData,
    'AES-256-CBC',
    $AesKeyData,
    OPENSSL_NO_PADDING,
    $InitializationVectorData
);

You really should use AES-GCM also, it is an authenticated mode and will surely be supported in PHP also.

rmhrisk
  • 1,814
  • 10
  • 16
  • The input length is 22, Encrypted output is 160 if converted to hex, 80 if it is urlencoded and 367 if base64 of uint8array. The raw input encrypted and send as arraybuffer to string is 32(block padding). The trade off using urlencoding which solves the problem seems to be the better, since only special chars are urlencoded. Believe your solution would increase the amount of data to send will be to much if eg. a picture is encrypted. Thanks any way :-) – gerteb May 01 '17 at 06:13
  • By the way I will look into aes-gcm. I am a newbee to encryption so all posibilities are to be explored. Re-use of iv is only ment for development purpose. – gerteb May 01 '17 at 06:26
  • My solution (using byte array) will work for all data, not just strings. Your running into the issue of ASCII vs Unicode. You can always Base64 encode the data though (which is all URLEncode is rally doing). – rmhrisk May 01 '17 at 06:51
  • You will also find binary data does not have the increased size, its just the nature of unicode see - http://stackoverflow.com/questions/10229156/how-many-characters-can-utf-8-encode – rmhrisk May 01 '17 at 06:59
  • what we will encrypt (len): 22, what we encode to (len): 22, our ciphertext (len): 32 – rmhrisk May 01 '17 at 07:13
  • Maybe the byte length of the ciphertext is 32, but when send with JSON.stringify the length is 534. Too much overhead in my opinion. – gerteb May 01 '17 at 12:38
  • The correct way to represent the data is binary array, your issue here is with unicode and json.stringify, see http://stackoverflow.com/questions/12271547/shouldnt-json-stringify-escape-unicode-characters – rmhrisk May 01 '17 at 18:08
  • I believe you are missing the point. Yes "The correct way to represent the data is binary array,", but if the input data is a large file, the amount of data to send between client and server will be ~10 times greater. That is why i want send it as utf-8. For some strange reason the utf-8 data has to be encoded with encodeURI(str), which gives ~100% overhead with many special chars. – gerteb May 03 '17 at 14:36
  • Only 1) if you encode it as ASCII prior to sending, 2) dont flatten the unicode. – rmhrisk May 04 '17 at 03:40
  • I just discovered that the problem is, that the text from my HTML input actually is ASCII even though headers and so on is declaring charset utf-8, and the input has to be converted to utf-8 using unescape(encodeURIComponent(str)) before encryption. It completely solved the problem. No prior encodeng and subsequent decodeng using only encodeURI :-) – gerteb May 04 '17 at 17:38
  • mrhrisk you were right. It was a ASCII vs Unicode problem. Thanks for the help. Now on to use it with aes-gcm ;-) – gerteb May 04 '17 at 17:43