Encrypt iOS and Decrypt Node.Js Aes

Encrypt iOS and Decrypt Node.js AES

  1. Are you sure the same key is being used in both libraries? You say you took out the SHA-256 part in AESCrypt, how is the library using the password parameter now? The AES algorithm can only use keys of 16, 24, or 32 bytes in length. Your password is 16 bytes long, but did you change the corresponding parameter to 128 (instead of 256) in the encrypt function?
    Do you know how CryptoJS is using the key parameter? Are you sure it's being used directly, or might there be some processing (for example, hashing) before it's passed to the underlying primitive AES encryption function?

  2. What mode of encryption is the CryptoJS library using? Its documentation doesn't say. Given that it asks for an IV, it's probably CBC, but you would have to look at the source to know for sure.
    AESCrypt's documentation claims to use CBC mode, but you don't give it an IV anywhere. That must mean that it generates it own somewhere, or always uses a fixed one. (Which half defeats the purpose of CBC mode, but that's another story). So you need to figure out what the IV actually is.

TL;DR: unless you make sure that the same key and key length, the same mode, and the same IV are used across both libraries, then you will have different cipher text.

Encrypt text in Node.js and Decrypt from iOS app

I've implemented this encryption logic using the built-in Node.js crypto module, I've encrypted the same plaintext using both the crypto-js function as well and decoded both to ensure the results are consistent:

const CryptoJS = require('crypto-js');
const crypto = require("crypto");

function encrypt_cryptojs(message, password, iv64, salt) {
var hash = CryptoJS.SHA256(salt);
var key = CryptoJS.PBKDF2(password, hash, { keySize: 256/32, iterations: 1000 });
var iv = CryptoJS.enc.Base64.parse(iv64);
var encrypted = CryptoJS.AES.encrypt(message, key, { iv: iv });
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}

// Use built-in crypto module.
function encrypt(message, password, iv64, salt) {
const iv = Buffer.from(iv64, 'base64');
const hash = crypto.createHash('sha256').update(salt, 'utf8').digest()
const key = crypto.pbkdf2Sync(password, hash, 1000, 32, null);

const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(message, 'utf8', 'base64')
encrypted += cipher.final('base64');
return encrypted;
}

function decrypt(messagebase64, password, iv64) {

const iv = Buffer.from(iv64, 'base64');
const hash = crypto.createHash('sha256').update(salt, 'utf8').digest()
const key = crypto.pbkdf2Sync(password, hash, 1000, 32, null);

const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(messagebase64, 'base64');
decrypted += decipher.final();
return decrypted;
}

const plaintext = "If you prick us do we not bleed? If you tickle us do we not laugh";
const salt = "some salt";
const password = crypto.scryptSync("some password", salt, 16).toString("base64");
const iv64 = "XxbSho8OZacvQwXC6S5RQw==";

console.log("Ciphertext (crypto js):", encrypt_cryptojs(plaintext, password, iv64, salt));
console.log("Ciphertext (built-in crypto module):", encrypt(plaintext, password, iv64, salt));
console.log("Decrypted (crypto js):", decrypt(encrypt_cryptojs(plaintext, password, iv64, salt), password, iv64));
console.log("Decrypted (built-in crypto module):", decrypt(encrypt(plaintext, password, iv64, salt), password, iv64));

iOS mobile app and Node.js web app AES 256 encryption

If you look at the source code for FBEncryptor, you'll see that it creates a 32-byte zero-filled buffer for the key and a 16-byte zero-filled buffer for the IV. The key is then copied into the key buffer. The IV buffer is untouched. In order to produce the same output via Node.js, we need to replicate what is happening inside FBEncryptor.

Instead of using crypto.createCipher, you'll need to use crypto.createCipheriv and supply the IV. Same goes for crypto.createDecipher.

So let's walkthrough the node.js code:

var crypto = require('crypto'); 
var key = "onceuponatime";
var toCrypt = "Hello World!";

This is unchanged from your original script. We simply import the crypto module and set up the encryption key and the string to be encrypted.

// Create the 32-byte zero-filled key buffer
keyBuf = new Buffer(Array(32));
// Copy the key into this buffer
keyBuf.write(key, 'utf8');

// Create the 16-byte zero-filled IV buffer
ivBuf = new Buffer(Array(16));

Here we create the key and IV buffers that we'll use to encrypt toCrypt.

var cipher = crypto.createCipheriv('aes256', keyBuf, ivBuf); 
output = cipher.update(toCrypt, 'utf-8', 'base64') + cipher.final('base64');
console.log(output);

Next, we set up the cipher with the key and IV buffers and encrypt toCrypt. This produces 7TsBLBvS6A1iByn9OTkzWA== which is the same as FBEncryptor.

var deCipher = crypto.createDecipheriv('aes256', keyBuf, ivBuf); 
decrypted = deCipher.update(output,'base64','utf-8') + deCipher.final('utf-8');
console.log(decrypted);

Here we set up the decipher with the key and IV buffers and decrypt the encrypted string. This produces the output Hello World!.

AES-256 produces different ciphertexts in iOS and Node.js with same password

You don't need to use CryptoJS, because node.js' crypto module provides everything you need for this to work. CryptoJS has a different binary representation than node.js' native Buffer, so there will be problem using both in conjunction.

Problems:

  • You're using crypto.createCipher() which will derive the key from a password on its own in an OpenSSL compatible format. You want to use crypto.createCipheriv().
  • You're not passing an IV to in Objective-C which defaults to a zero filled IV. You need to do the same in node.js by initializing a zero-filled Buffer.
  • You provide the key in Base64 encoded form in node.js, but you have to provide the bytes (Buffer).
  • Since the key size is 256 bit you're actually using AES-256 and not AES-128. The CommonCrypto code seems to change automatically to 256 bit despite specifying 128 bit, but node.js requires you to specify 256 bit explicitly. Also, "aes128" or "aes256" will default to ECB mode in node.js, but CommonCrypto defaults to CBC mode, so you need to explicitly specify this.

Full working code:

var crypto = require('crypto');
var password = "1234567890123456";
var salt = "gettingsaltyfoo!";

var sha256 = crypto.createHash("sha256");
sha256.update(salt);
var hash = sha256.digest();

var key = crypto.pbkdf2Sync(password, hash, 1000, 32, "sha1");

var iv = new Buffer(16);
iv.fill(0);

var algorithm = 'aes-256-cbc';

function encrypt(text){
var cipher = crypto.createCipheriv(algorithm, key, iv);
var crypted = cipher.update(text,'utf8','base64');
crypted += cipher.final('base64');
return crypted;
}

function decrypt(text){
var decipher = crypto.createDecipheriv(algorithm, key, iv);
var dec = decipher.update(text,'base64','utf8');
dec += decipher.final('utf8');
return dec;
}

console.log(encrypt("Hello World"));

Output:


vfOzya0yV9G5hLHeSh3R1g==

Other considerations:

  • You need to generate a random IV for every encryption that you do. If you don't do this, then an attacker may see that you encrypted the same message multiple times without actually decrypting it if you use the same key every time. Since you derive a key from a password, then you can do this a bit better by generating a random salt and derive 384 bit (48 byte) from PBKDF2. Use the first 32 byte for the key and the rest for the IV.

  • You need to authenticate the ciphertexts. If you don't then an attacker might mount a padding oracle attack on your system. You can easily do this by running an HMAC over the ciphertext and send the resulting tag along with it. You can then verify the tag before decryption by running the HMAC again over the received ciphertext in order to check for manipulation.

    Or you could use an authenticated mode like GCM.

Unable decrypt data in NodeJS which is encrypted using Swift CryptoKit - AES-256-GCM

In addition to the fact that Java crypto puts the GCM authtag at the end of the ciphertext (as identified in comments) your Java code is using the SHA256 of the password as the key, while your nodejs is using the ASCII characters of the hex representation of half the SHA256; this is a completely different value, and the whole point of symmetric (traditional) cryptography is that you must use (exactly) the same key at both ends. In addition your method of converting to base64 and then concatenating with string-plus, and conversely slicing the base64 before decoding, only works right if the data and IV are both multiples of 3: the GCM IV/nonce is 12, which is okay, and so is your example value 'This is data', but most real data won't be.

The following modified js matches your Java. I don't do SWIFT, but if as you say it matches your Java it should also match this js.

const crypto = require('crypto')
function encryptData(data,password){
//--let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest('hex').slice(0,32).toLowerCase();
let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest();
let iv = Buffer.from('mBj0tzBUxDFmix1T', 'base64'); // TEST ONLY SHOULD BE UNIQUE (such as random)
let cipher = crypto.createCipheriv('aes-256-gcm', password_hash, iv);
//--let encryptedData = Buffer.from(cipher.update(data, 'utf8', 'hex') + cipher.final('hex'), 'hex');
let encryptedData = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
//--return iv.toString('base64') + cipher.getAuthTag().toString('base64') + encryptedData.toString('base64');
return Buffer.concat([iv,encryptedData,cipher.getAuthTag()]).toString('base64');
// or just concat([iv,cipher.update(data,'utf8'),cipher.final(),cipher.getAuthTag()]).toString('base64')
}

function decryptData(data,password){
let password_hash = crypto.createHash('sha256').update(password, 'utf-8').digest(); //**
let combinerBuffer = Buffer.from(data, 'base64'); //**
let iv = combinerBuffer.slice(0,12); //**
let deciper = crypto.createDecipheriv('aes-256-gcm', password_hash, iv);
let temp = combinerBuffer.length-16;
deciper.setAuthTag(combinerBuffer.slice(temp));
return deciper.update(combinerBuffer.slice(12,temp), 'utf8') + deciper.final('utf8');
}

let p = 'password', i = 'This is data';
let c = encryptData(i,p); console.log(c);
let d = decryptData(c,p); console.log(d);

Finally, using a single fast and unsalted hash of a password for a key has very little security and will likely be broken. But that's a design issue, and offtopic for SO. If you have the power to change this design and care about actual security, see security.SX, where you will find many recommendations to at least use something like PBKDF2 (an iterated, salted HMAC) or even better one of the newer, memory-hard password hashes like scrypt or argon2.

Also, as I commented, the IV/nonce for GCM is only required to be unique; using a secure random generator is one common way of obtaining unique values, but not the only one. (This contrasts to CBC mode where the IV must be unique and unpredictable, which in practice requires either random or SIV.)

Swift encryption and NodeJS decryption producing inconsistent results

Look for cross plate-form AES encryption

Encrypt and Decrypt iOS/Node.js Security Inquiry

Typically a random salt is used and prepended to the encrypted data. It is also common to all prepend the PBKDF2 iteration count along with a version number helps for future-proofing. Finally, skipping an iv reduces the protection of the first block and you might consider an authentication hash.

This is similar to what RNCryptor does. See RNCryptor-Spec-v3.md for a detail of a encrypted message.

Notes:

I don't understand CC_SHA256 of the salt, that shouldn't be necessary.

NSData* outputMessage = [NSMutableData dataWithBytes:decrypted.mutableBytes
length:bytesDecrypted];

is unnecessary, just set the length of decrypted
decrypted.length = bytesDecrypted;
and use decrypted in place of outputMessage.

Swift CryptoKit AES Encryption and Javascript AES Decryption

CryptoJS uses CBC mode by default, and doesn’t support GCM at all. You shouldn’t use CryptoJS at the best of times (the native and better-designed Web Crypto API is to be preferred), but especially not on the server, where Node.js has always had a native crypto module.

First, include the GCM nonce and tag, which are essential components:

return val.combined!.base64EncodedString()

Then, in Node.js, using the layout as described in the documentation for the combined property:

The data layout of the combined representation is: nonce, ciphertext, then tag.

// where `sealedBox` is a buffer obtained with `Buffer.from(encryptedString, 'base64')`
let nonce = sealedBox.slice(0, 12);
let ciphertext = sealedBox.slice(12, -16);
let tag = sealedBox.slice(-16);
let decipher = crypto.createDecipheriv('aes-128-gcm', key, nonce);
decipher.setAuthTag(tag);
let decrypted =
Buffer.concat([decipher.update(ciphertext), decipher.final()])
.toString('utf8');

Once that’s working, don’t forget to fix your key, because .init(data: Array(key.utf8)) is very uncomfortable (your AES keys should not be valid UTF-8).

  • If you’re starting with a password (for a good reason, not just because it seemed convenient), use a PBKDF to get key bytes. (Unfortunately, no PBKDF implementations are built into CryptoKit.)

    … but if the good reason is that it’s a user-provided password and you’re claiming to provide security, please get someone experienced with use of cryptography to review your work.

  • Otherwise, generate a random key safely and decode it from a Base64 or hex string. No UTF-8.

    node -p 'crypto.randomBytes(16).toString("base64")'

    And get someone experienced with use of cryptography to review your work anyway.

AES 256 CTR Encryption in Golang Decrypt in Node JS with CryptoJS and the Key is String (not WordArray)

The code is mostly OK, there are just a few minor issues:

  1. CryptoJS does not automatically disable the default PKCS7 padding for stream cipher modes like CTR. Therefore, PKCS7 padding must be applied in the Go code.
  2. Since the CryptoJS code uses the internal PBKDF, the OpenSSL format is required for decryption (i.e. the ASCII encoding of Salted__ followed by the 8 bytes salt and the actual ciphertext), hex encoded. So the Go code must format and encode the data accordingly.
  3. CryptoJS derives key and IV when using the internal PBKDF. Therefore, in the CryptoJS code, the specified IV is ignored during decryption. Hence, in the Go code, any IV can be specified.

The following code corresponds to your code, extended by the PKCS#7 padding and the formatting/encoding of the result (consider the comments in the code). Note that, as in your code, a hard-coded salt is used for simplicity, but in practice a randomly generated salt must be applied for security reasons:

package main

import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"fmt"

evp "github.com/walkert/go-evp"
"github.com/zenazn/pkcs7pad"
)

func main() {
rawKey := "46ca2a49c8074dadb99843f6b86c5975"
data := pkcs7pad.Pad([]byte("the quick brown fox jumps over the lazy dog"), 16) // 1. Pad the plaintext with PKCS#7
fmt.Println("padded data: ", hex.EncodeToString(data))

encryptedData := encrypt(rawKey, data)
fmt.Println("encrypted data: ", encryptedData)
}

func encrypt(rawKey string, plainText []byte) string {
salt := []byte("ABCDEFGH") // hardcoded at the moment

// Gets key and IV from raw key.
key, iv := evp.BytesToKeyAES256CBCMD5([]byte(salt), []byte(rawKey))

// Create new AES cipher block
block, err := aes.NewCipher(key)
if err != nil {
return err.Error()
}

cipherText := make([]byte, len(plainText))

// Encrypt.
encryptStream := cipher.NewCTR(block, iv)
encryptStream.XORKeyStream(cipherText, plainText)

ivHex := hex.EncodeToString(iv)
encryptedDataHex := hex.EncodeToString([]byte("Salted__")) + hex.EncodeToString(salt) + hex.EncodeToString(cipherText) // 2. Apply the OpenSSL format, hex encode the result
return ivHex + ":" + encryptedDataHex // 3. Any value for ivHex can be used here, e.g. "00000000000000000000000000000000"
}

The output is:

padded data:  74686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f670505050505
encrypted data: 3a010df5e7985f2d8b0c00e3a096347f:53616c7465645f5f41424344454647486036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd82826f3d3c5

This ciphertext can be decrypted with the legacy code:

const decrypt = function (rawKey, encryptedData) {
const split = encryptedData.split(':');
if (split.length < 2) return '';

const reb64 = CryptoJS.enc.Hex.parse(split[1]);
const bytes = reb64.toString(CryptoJS.enc.Base64);

const hash = CryptoJS.AES.decrypt(bytes, rawKey, {
iv: split[0], // This is ignored if the internal PBKDF is used
mode: CryptoJS.mode.CTR
});
const plain = hash.toString(CryptoJS.enc.Utf8);
return plain;
}

const rawKey = '46ca2a49c8074dadb99843f6b86c5975';
const encryptedData = '3a010df5e7985f2d8b0c00e3a096347f:53616c7465645f5f41424344454647486036327f61cf3050fddd6ea76325148c81e170a63b514b8818afbbb894c874c87cc4c865300c7b2d0e0fd82826f3d3c5';

const decyptedData = decrypt(rawKey, encryptedData);
document.getElementById("pt").innerHTML = "decrypted data: " + decyptedData;
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<p style="font-family:'Courier New', monospace;" id="pt"></p>

Node.js AES decrypt an iOS encrypted NSString

In Objective-C you're not defining the IV which defaults to a zero filled IV. Node.js says that

key and iv must be 'binary' encoded strings or buffers.

The character 0 in your IV string is not the same as the byte \0. You're not passing a zero filled IV, but an IV filled with 0x30 bytes.

Fill the IV like this:

var iv = new Buffer(16);
iv.fill(0);


Related Topics



Leave a reply



Submit