Maarten covers most of the major points that I wanted to make, so this is just some elaboration and an example of it in Node.
The changes from your code are:
- Encode in Base64 rather than Hex. This is much more space-efficient. It would be even better to just use Buffers and not create a string at all; then we could have a 9-byte nonce rather than a 5-byte nonce.
- Get rid of separator character between the IV/nonce and the ciphertext. We know how long the IV/nonce is; we don't need a separator.
- Use CTR mode rather than CBC mode. This makes output length equal to input length.
- Use a nonce rather than an IV. Randomly choose the nonce from a 2^40 space. (Randomly choosing a CTR nonce is very dangerous in general. See below for why it might be acceptable in your use case; it is still never recommended.)
- Fix password generation by adding PBKDF2 (you could also just use randomBytes). Your password is highly insecure. It's an ASCII string, which means it represents a tiny fraction of the AES-256 keyspace. On the order of 0.000000000002% of the keyspace. That's how much less secure this is than AES-256.
As Maarten notes, it is quite dangerous for CTR mode to duplicate a Key+Nonce pair. If someone does that, they can learn the XOR of the two original messages. With that, they have a good chance of decrypting both messages. For example, if you duplicated your key+nonce on two messages and the attacker used that to discover that their XOR was 3 and knew that the encrypted text was a capital letter, they would know that the two messages had to be one of these:
[('A', 'B'), ('D', 'G'), ('E', 'F'), ('H', 'K'), ('I', 'J'), ('L', 'O'),
('M', 'N'), ('P', 'S'), ('Q', 'R'), ('T', 'W'), ('U', 'V')]
This kind of information is devastating for structured data like human language or computer protocols. It can very quickly be used to decrypt the whole message. Key+nonce reuse is how WEP was broken. (When you do this by hand, it's basically identical to solving a cryptogram puzzle you'd find in the newspaper.) It is less powerful the more random the encrypted data is, and the less context it provides.
With a random 5-byte nonce, there is a 50% likelihood of a collision after about 1.3M encryptions. With a random 8-byte nonce, there is a 50% likelihood of a collision after about 5.3B encryptions. sqrt(pi/2 * 2^bits)
In cryptographic terms, this is a completely broken. It may or may not be sufficient for your purposes. To do it correctly (which I do not do below), as Maarten notes, you should keep track of your counter and increment it for every encryption rather than using a random one. After 2^40 encryptions (~1T), you change your key.
Assuming that leaking information about two messages per million is acceptable, this is how you would implement that.
const crypto = require('crypto');
const ENCRYPTION_KEY = 'Must256bytes(32characters)secret';
const SALT = 'somethingrandom';
const IV_LENGTH = 16;
const NONCE_LENGTH = 5; // Gives us 8-character Base64 output. The higher this number, the better
function encrypt(key, text) {
let nonce = crypto.randomBytes(NONCE_LENGTH);
let iv = Buffer.alloc(IV_LENGTH)
nonce.copy(iv)
let cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
let encrypted = cipher.update(text.toString());
message = Buffer.concat([nonce, encrypted, cipher.final()]);
return message.toString('base64')
}
function decrypt(key, text) {
let message = Buffer.from(text, 'base64')
let iv = Buffer.alloc(IV_LENGTH)
message.copy(iv, 0, 0, NONCE_LENGTH)
let encryptedText = message.slice(NONCE_LENGTH)
let decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
let decrypted = decipher.update(encryptedText);
try{
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}catch(Err){
return 'NULL';
}
}
// You could do this one time and record the result. Or you could just
// generate a random 32-byte key and record that. But you should never
// pass an ASCII string to the encryption function.
let key = crypto.pbkdf2Sync(ENCRYPTION_KEY, SALT, 10000, 32, 'sha512')
let encrypted = encrypt(key, "X")
console.log(encrypted + " : " + encrypted.length)
let decrypted = decrypt(key, encrypted)
console.log(decrypted)