I want to read from a stream then encrypt it and finally write it to another file.
This is my code:
var fs = require('fs');
var crypto = require('crypto');
var infile = fs.createReadStream('a.dmg');
var outfile = fs.createWriteStream('b.dmg');
var encrypt = crypto.createCipher('aes192', 'behdad');
var size = fs.statSync('a.dmg').size;
console.log(size);
infile.on('data',function(data) {
var percentage = parseInt(infile.bytesRead) / parseInt(size);
console.log(percentage * 100);
var encrypted = encrypt.read(data);
console.log(encrypted);
if(encrypted){
console.log(encrypted);
outfile.write(encrypted);
}
});
infile.on('close', function() {
encrypt.end();
outfile.close();
});
But it returns an empty file, and encrypted is null. What is the problem? I don't want to use pipe .
You really want to use Cipher#update and Cipher#final instead of Stream#read, because the function signature is read([size]) and data is not a size.
var fs = require('fs');
var crypto = require('crypto');
var infile = fs.createReadStream('a.dmg');
var outfile = fs.createWriteStream('b.dmg');
var encrypt = crypto.createCipher('aes192', 'behdad');
var size = fs.statSync('a.dmg').size;
console.log(size);
infile.on('data',function(data) {
var percentage = parseInt(infile.bytesRead) / parseInt(size);
console.log(percentage * 100);
var encrypted = encrypt.update(data);
console.log(encrypted);
if(encrypted){
console.log(encrypted);
outfile.write(encrypted);
}
});
infile.on('close', function() {
outfile.write(encrypt.final());
outfile.close();
});
Since crypto.createCipher is deprecated now. You should use crypto.createCipheriv where you provide a key and IV. That means that you should stretch the password that you use with PBKDF2 or similar to get a key and generate a random IV to get semantic security. Since the salt for PBKDF2 and the IV are not supposed to be secret, they can be written in front of the ciphertext. Since they have always the same length (salt is usually 8-16 bytes and IV always 16 bytes for AES-CBC), you know how many bytes you have to read in order to get those values back. Keep in mind that the decryption code has to have proper error handling.
Related
I'm trying to port the following Go functions to nodeJS using crypt or crypt-js but i'm having issues trying to figure out what's wrong:
The Go encryption code is available at https://go.dev/play/p/O88Bslwd-qh ( both encrypt and decrypt work)
The current nodejs implementation is:
var decryptKey= "93D87FF936DAB334C2B3CC771C9DC833B517920683C63971AA36EBC3F2A83C24";
const crypto = require('crypto');
const algorithm = 'aes-256-cfb';
const BLOCK_SIZE = 16;
var message = "8a0f6b165236391ac081f5c614265b280f84df882fb6ee14dd8b0f7020962fdd"
function encryptText(keyStr, text) {
const hash = crypto.createHash('sha256');
//Decode hex key
keyStr = Buffer.from(keyStr, "hex")
hash.update(keyStr);
const keyBytes = hash.digest();
const iv = crypto.randomBytes(BLOCK_SIZE);
const cipher = crypto.createCipheriv(algorithm, keyBytes, iv);
cipher.setAutoPadding(true);
let enc = [iv, cipher.update(text,'latin1')];
enc.push(cipher.final());
return Buffer.concat(enc).toString('hex');
}
function decryptText(keyStr, text) {
const hash = crypto.createHash('sha256');
//Decode hex key
keyStr = Buffer.from(keyStr, "hex")
hash.update(keyStr);
const keyBytes = hash.digest();
const contents = Buffer.from(text, 'hex');
const iv = contents.slice(0, BLOCK_SIZE);
const textBytes = contents.slice(BLOCK_SIZE);
const decipher = crypto.createDecipheriv(algorithm, keyBytes, iv);
decipher.setAutoPadding(true);
let res = decipher.update(textBytes,'latin1');
res += decipher.final('latin1');
return res;
}
console.log(message)
result = decryptText(decryptKey,message);
console.log(result);
message = encryptText(decryptKey,'hola').toString();
console.log(message)
result = decryptText(decryptKey,message);
console.log(result);
Any idea why it is not working as expected?
Note: I know that padding is not required with cfb but i can't modify the encryption code, it just for reference.
I don't know Go or the specifics of aes.NewCipher(key), but from its documentation it doesn't look like it's hashing the key in any way. The Go code you're linking to also doesn't hash it, so I'm not sure why you're hashing it in the Node.js code.
This should be sufficient:
function encryptText(keyStr, text) {
const keyBytes = Buffer.from(keyStr, "hex")
…
}
function decryptText(keyStr, text) {
const keyBytes = Buffer.from(keyStr, 'hex');
…
}
As an aside: it looks like you may be encrypting JSON blocks with these functions. If so, I would suggest not using any encoding (like latin1) during the encryption/decryption process, given that JSON text must be encoded using UTF-8.
I've Encrypted my text by a key in Client by AES-256-GCM algorithm and I can decrypt it in Client, But when I send it to the Backend which has a SharedKey(the same as the Client has), it can decrypt the message by AES-256-CTR algorithm(I used this algo because the AES-256-GCM in Nodejs needs authTag that I don't create it in Client and iv is the only thing I have).
When I decrypt the message on the Backend side, it works with no error, but the result is not what I encrypted in the Client
Here is what I wrote:
Client:
async function encrypt(text: string) {
const encodedText = new TextEncoder().encode(text);
const aesKey = await generateAesKey();
const iv = window.crypto.getRandomValues(
new Uint8Array(SERVER_ENCRYPTION_IV_LENGTH)
);
const encrypted = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
},
aesKey,
encodedText
);
const concatenatedData = new Uint8Array(
iv.byteLength + encrypted.byteLength
);
concatenatedData.set(iv);
concatenatedData.set(new Uint8Array(encrypted), iv.byteLength);
return arrayBufferToBase64(concatenatedData),
}
Backend:
export function decrypt(sharedKey: string, message: string) {
const messageBuffer = new Uint8Array(base64ToArrayBuffer(message));
const iv = messageBuffer.subarray(0, 16);
const data = messageBuffer.subarray(16);
const decipher = crypto.createDecipheriv(
'aes-256-ctr',
Buffer.from(sharedKey, 'base64'),
iv
);
const decrypted =
decipher.update(data, 'binary', 'hex') + decipher.final('hex');
return Buffer.from(decrypted, 'hex').toString('base64');
}
Sample usage:
const encrypted = encrypt("Hi Everybody");
// send the encrypted message to the server
// Response is: Ô\tp\x8F\x03$\f\x91m\x8B B\x1CkQPQ=\x85\x97\x8AêsÌG0¸Ê
Since GCM is based on CTR, decryption with CTR is in principle also possible. However, this should generally not be done in practice, since it skips the authentication of the ciphertext, which is the added value of GCM over CTR.
The correct way is to decrypt on the NodeJS side with GCM and properly consider the authentication tag.
The authentication tag is automatically appended to the ciphertext by the WebCrypto API, while the crypto module of NodeJS handles ciphertext and tag separately. Therefore, not only the nonce but also the authentication tag must be separated on the NodeJS side.
The following JavaScript/WebCrypto code demonstrates the encryption:
(async () => {
var nonce = crypto.getRandomValues(new Uint8Array(12));
var plaintext = 'The quick brown fox jumps over the lazy dog';
var plaintextEncoded = new TextEncoder().encode(plaintext);
var aesKey = base64ToArrayBuffer('a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=');
var aesCryptoKey = await crypto.subtle.importKey('raw', aesKey, 'AES-GCM', true, ['encrypt', 'decrypt']);
var ciphertextTag = await crypto.subtle.encrypt({name: 'AES-GCM', iv: nonce}, aesCryptoKey, plaintextEncoded);
ciphertextTag = new Uint8Array(ciphertextTag);
var nonceCiphertextTag = new Uint8Array(nonce.length + ciphertextTag.length);
nonceCiphertextTag.set(nonce);
nonceCiphertextTag.set(ciphertextTag, nonce.length);
nonceCiphertextTag = arrayBufferToBase64(nonceCiphertextTag.buffer);
document.getElementById("nonceCiphertextTag").innerHTML = nonceCiphertextTag; // ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
})();
// Helper
// https://stackoverflow.com/a/9458996/9014097
function arrayBufferToBase64(buffer){
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
// https://stackoverflow.com/a/21797381/9014097
function base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
<p style="font-family:'Courier New', monospace;" id="nonceCiphertextTag"></p>
This code is basically the same as your code, with some changes needed because of methods you didn't post like generateAesKey() or arrayBufferToBase64().
Example output:
ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=
The following NodeJS/crypto code demonstrates the decryption. Note the tag separation and explicit passing with setAuthTag():
var crypto = require('crypto');
function decrypt(key, nonceCiphertextTag) {
key = Buffer.from(key, 'base64');
nonceCiphertextTag = Buffer.from(nonceCiphertextTag, 'base64');
var nonce = nonceCiphertextTag.slice(0, 12);
var ciphertext = nonceCiphertextTag.slice(12, -16);
var tag = nonceCiphertextTag.slice(-16); // Separate tag!
var decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag); // Set tag!
var decrypted = decipher.update(ciphertext, '', 'utf8') + decipher.final('utf8');
return decrypted;
}
var nonceCiphertextTag = 'ihAdhr6595oyQ3koj52cnZp7VeB1fzWuY1v7vqFdSQGxK0VQxIXUegB1mVG4rC5Aymij7bQ9rmnFWbpo7C2znN4ROnnChB0=';
var key = 'a068Sk+PXECrysAIN+fEGDzMQ3xlpWgE1bWXHVLb0AQ=';
var decrypted = decrypt(key, nonceCiphertextTag);
console.log(decrypted);
Output:
The quick brown fox jumps over the lazy dog
For completeness: Decryption of a GCM ciphertext with CTR is also possible by appending 4 bytes to the 12 bytes nonce (0x00000002). For other nonce sizes the relation is more complex, see e.g. Relationship between AES GCM and AES CTR. However, as already said, this should not be done in practice, since it bypasses the authentication of the ciphertext and is thus insecure.
I have a usecase where I want to encrypt my data first with a symmetric key and then encrypt the symmetric key with the client's public key. I tried to mock the scenario, but getting 'Invalid RSAES-OAEP padding' error.
Code:
var forge = require('node-forge');
var _crypto = require('crypto');
var rsa = forge.pki.rsa;
var masterKey = _crypto.randomBytes(32);
var keypair = rsa.generateKeyPair(2048);
var encryptedVal = keypair.publicKey.encrypt(masterKey, 'RSA-OAEP');
var decryptedVal = keypair.privateKey.decrypt(encryptedVal, 'RSA-OAEP');
console.log(masterKey,decryptedVal)
Do I have to convert the symmetric key format before encrypting it? And if yes, to what?
masterKey is a Buffer. You need to convert it to bytes so forge can use it
Try this function (I didn't test it...)
function toBytes(buf) {
var byteString = '';
for (var i = 0; i < buf.length; ++i) {
byteString += String.fromCodePoint(buf[i]);
}
return byteString;
}
Usage
var masterKey = toBytes(_crypto.randomBytes(32));
Code Sample is as follows:
var crypto = require('crypto');
var key = 'ExchangePasswordPasswordExchange';
var plaintext = '150.01';
var iv = new Buffer(crypto.randomBytes(16))
ivstring = iv.toString('hex');
var cipher = crypto.createCipheriv('aes-256-cbc', key, ivstring)
var decipher = crypto.createDecipheriv('aes-256-cbc', key,ivstring);
cipher.update(plaintext, 'utf8', 'base64');
var encryptedPassword = cipher.final('base64');
Getting error of invalid IV length.
From https://github.com/nodejs/node/issues/6696#issuecomment-218575039 -
The default string encoding used by the crypto module changed in
v6.0.0 from binary to utf8. So your binary string is being interpreted
as utf8 and is most likely becoming larger than 16 bytes during that
conversion process (rather than smaller than 16 bytes) due to invalid
utf8 character bytes being added.
Modifying your code so that ivstring is always 16 characters in length should solve your issue.
var ivstring = iv.toString('hex').slice(0, 16);
The above answer adds more overhead than needed, since you converted each byte to a hexidecimal representation that requires twice as many bytes all you need to do is generate half the number of bytes
var crypto = require('crypto');
var key = 'ExchangePasswordPasswordExchange';
var plaintext = '150.01';
var iv = new Buffer(crypto.randomBytes(8))
ivstring = iv.toString('hex');
var cipher = crypto.createCipheriv('aes-256-cbc', key, ivstring)
var decipher = crypto.createDecipheriv('aes-256-cbc', key,ivstring);
cipher.update(plaintext, 'utf8', 'base64');
var encryptedPassword = cipher.final('base64');
In Node.js 10 I had to use a 12 bytes string for it to work... const iv = crypto.pseudoRandomBytes(6).toString('hex');. 16 bytes gave me an error. I had this problem when I was running Node.js 10 globally, and then uploading it to a Cloud Functions server with Node.js 8. Since Cloud Functions have Node.js 10 in beta, I just switched to that and now it works with the 12 bytes string. It didn't even work with a 16 bytes string on Node.js 8 on the Cloud Functions server...
When you really need Key/Iv from legacy Crypto
In case of cypher aes-256-cbc, required length for Key and IV is 32 Bytes and 16 Bytes.
You can calculate Key length by dividing 256 bits by 8 bits, equals 32 bytes.
Following GetUnsafeKeyIvSync(password) uses exactly same behavior as previous crypto did in old days.
There is no salt, and single iteration with MD5 digest, so anyone can generate exactly same Key and Iv.
This is why deprecated.
However, you may still need to use this approach only if your encrypted data is stored and cannot be changed(or upgraded.).
Do NOT use this function for new project.
This is provided only for who cannot upgrade previously encrypted data for other reason.
import * as crypto from 'node:crypto';
import { Buffer } from 'node:buffer';
export function GetUnsafeKeyIvSync(password) {
try {
const key1hash = crypto.createHash('MD5').update(password, 'utf8');
const key2hash = crypto.createHash('MD5').update(key1hash.copy().digest('binary') + password, 'binary');
const ivhash = crypto.createHash('MD5').update(key2hash.copy().digest('binary') + password, 'binary');
const Key = Buffer.from(key1hash.digest('hex') + key2hash.digest('hex'), 'hex');
const IV = Buffer.from(ivhash.digest('hex'), 'hex');
return { Key, IV };
}
catch (error) {
console.error(error);
}
}
export function DecryptSync(data, KeyIv) {
let decrypted;
try {
const decipher = crypto.createDecipheriv('aes-256-cbc', KeyIv.Key, KeyIv.IV);
decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
}
catch (error) {
console.error(error);
decrypted = '';
}
return decrypted;
}
export function EncryptSync(data, KeyIv) {
let encrypted;
try {
const cipher = crypto.createCipheriv('aes-256-cbc', KeyIv.Key, KeyIv.IV);
encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
}
catch (error) {
console.error(error);
encrypted = '';
}
return encrypted;
}
For testing,
export function Test() {
const password = 'my plain text password which is free from length requirment';
const data = 'this is data to be encrypted and decrypted';
// Use same logic to retrieve as legacy crypto did.
// It is unsafe because there is no salt and single iteration.
// Anyone can generate exactly same Key/Iv with the same password.
// We would only need to use this only if stored encrypted data must be decrypted from previous result.
// Do NOT use this for new project.
const KeyIv = GetUnsafeKeyIvSync(password);
// Key is in binary format, for human reading, converted to hex, but Hex string is not passed to Cypher.
// Length of Key is 32 bytes, for aes-256-cbc
console.log(`Key=${KeyIv.Key.toString('hex')}`);
// Key is in binary format , for human reading, converted to hex, but Hex string is not passed to Cypher.
// Length of IV is 16 bytes, for aes-256-cbc
console.log(`IV=${KeyIv.IV.toString('hex')}`);
const encrypted = EncryptSync(data, KeyIv);
console.log(`enc=${encrypted}`);
const decrypted = DecryptSync(encrypted, KeyIv);
console.log(`dec=${decrypted}`);
console.log(`Equals ${decrypted === data}`);
return decrypted === data;
}
How to create a initialization vector (IV) from a random source in NodeJS, like I do in PHP as follows:
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_RAND);
In NodeJS I thought crypto.createCipheriv could help but no.
You are on the right track and will still need to use:
Crypto.createCipheriv()
The function that can generate you a randomized initialization vector would be:
Crypto.randomBytes(16)
The 16 signifies the number of bytes required to fulfil the required length of the vector.
To give an example:
var iv = Crypto.randomBytes(16);
var cipher = Crypto.createCipheriv('aes-128-cbc', new Buffer(<128 bit password>), iv);
var encrypted = cipher.update(clearText);
var finalBuffer = Buffer.concat([encrypted, cipher.final()]);
//Need to retain IV for decryption, so this can be appended to the output with a separator (non-hex for this example)
var encryptedHex = iv.toString('hex') + ':' + finalBuffer.toString('hex')
For the sake of completeness, here's an example for decrypting the above encryptedHex
var encryptedArray = encryptedHex.split(':');
var iv = new Buffer(encryptedArray[0], 'hex');
var encrypted = new Buffer(encryptedArray[1], 'hex');
var decipher = Crypto.createDecipheriv('aes-128-cbc', new Buffer(<128 bit password>), iv);
var decrypted = decipher.update(encrypted);
var clearText = Buffer.concat([decrypted, decipher.final()]).toString();