AES 256 GCM encryption decryption does not work on android device - node.js

I'm working on a react-native project and trying to encrypt and decrypt the photo by crypto and using aes-256-gcm algorithm. This code works well on simulator both android and ios, these is no issue on device when I'm debugging as well, but as soon as I stop Remote JS Debugging on android device, this error'll appear: unsupported state or unable to authenticate data.
I'm completely confuse and I don't know how I can fix this issue.
I should mention that this code'll work correctly for small data and string, only there is problem with large file.
Here is my code:
key = crypto.randomBytes(32);
static encryptFile = inData => {
let iv = Buffer.from(crypto.randomBytes(16));
let cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(inData, "binary", "hex");
encrypted += cipher.final("hex");
let cipherTag = cipher.getAuthTag();
encrypted += "," + iv.toString("hex") + "," + cipherTag.toString("hex");
return encrypted;
};
static decryptFile = inEncData => {
let encParts = inEncData.split(",");
let currentIV = Buffer.from(encParts[1], "hex");
let currentTag = Buffer.from(encParts[2], "hex");
let decipher = crypto.createDecipheriv(algorithm, key, currentIV);
decipher.setAuthTag(currentTag);
let decrypted = decipher.update(encParts[0], "hex");
let decryptedFinal = decipher.final();
Buffer.concat([decrypted, decryptedFinal]);
return decrypted;
};
I read image file by rn-fetch-blob and pass it to encryptFile method:
let res = await RNFetchBlob.fs.readFile(filePath, "ascii");
let enc = convertor.encryptFile(res);
let dec = convertor.decryptFile(enc);
And the error occurs on decryptFile method.

Just add utf-8 with hex encoding parameters shown below:
let encrypted = cipher.update(inData, "utf8", "hex");
encrypted += cipher.final("hex");
let decrypted = decipher.update(encParts[0], "hex","utf8");
let decryptedFinal = decipher.final('utf8');

Related

NodeJS AESCFB + pkcs7 padding decryption

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.

Decrypting AES-256-CBC Cipher using Node.js

I'm trying to decrypt an AES-256-CBC ciphertext from a web service using Node.js but can't seem to make it work and honestly lost.
const raw = `{
"iv":"uUwGJgxslfYiahji3+e2jA==",
"docMimeType":"text\/xml",
"doc":"1XLjWZlMMrgcpR6QtfwExQSOOPag1BJZTo1QEkcDrY6PFesWoVw8xrbHFsEYyMVDeemzk+5kBnb3\r\nqBmcUtkSFs7zDsxjYZkkEU9nyq1jXFz99fGylIealw37FPMaK0gviXESRO5AHMs46tpgSQcuWX0Z\r\nV7+mnTvjmaRHi4p1Cvg8aYfDO1aIWWWjAwOTCyopyCwribbGoEdiYDc5pERHpw=="
}`;
const cleanedEncryptedDataAsJsonStr = raw.replace(/\r?\n|\r/g, " ")
const data = JSON.parse(cleanedEncryptedDataAsJsonStr)
const ivBase64 = data.iv
const iv = Buffer.from(ivBase64, 'base64').toString('hex').substring(0, 16)
const plainKey = 'PHilheaLthDuMmyciPHerKeyS'
const hashKey = crypto.createHash('sha256');
hashKey.update(plainKey)
const key = hashKey.digest('hex').substring(0, 32)
let encrypted = Buffer.from(data.doc, 'base64').toString('hex')
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
let decrypted = decipher.update(encrypted, 'hex', 'utf-8')
decrypted += decipher.final('utf-8')
so I'm receiving a json object which contains the iv and the encrypted data(doc), and I have a copy of the cipher key which according to the docs needs to be hashed during the decryption.
doc: base64 encoding of the encrypted data
iv: base64 encoded value of initialization vector during encryption
When I run my code, the error is:
Error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length
and also not sure on how to handle \r\n in the raw json string.
The decrypted message should be:
<eEMPLOYERS ASOF="07-02-2022"><employer pPEN="110474000002" pEmployerName="JOSE A TERAMOTO ODM" pEmployerAddress="ORANBO, PASIG CITY"/></eEMPLOYERS
There are the following issues in the code:
IV and key must not be hex encoded.
The default PKCS#7 padding must be disabled since Zero padding was applied during encryption (if desired, explicitly remove the trailing 0x00 padding bytes).
Fixed code:
var crypto = require('crypto')
const raw = `{
"iv":"uUwGJgxslfYiahji3+e2jA==",
"docMimeType":"text\/xml",
"doc":"1XLjWZlMMrgcpR6QtfwExQSOOPag1BJZTo1QEkcDrY6PFesWoVw8xrbHFsEYyMVDeemzk+5kBnb3\r\nqBmcUtkSFs7zDsxjYZkkEU9nyq1jXFz99fGylIealw37FPMaK0gviXESRO5AHMs46tpgSQcuWX0Z\r\nV7+mnTvjmaRHi4p1Cvg8aYfDO1aIWWWjAwOTCyopyCwribbGoEdiYDc5pERHpw=="
}`;
const cleanedEncryptedDataAsJsonStr = raw.replace(/\r?\n|\r/g, " ")
const data = JSON.parse(cleanedEncryptedDataAsJsonStr)
const ivBase64 = data.iv
const iv = Buffer.from(ivBase64, 'base64') // .toString('hex').substring(0, 16) // Fix 1: no hex encoding
const plainKey = 'PHilheaLthDuMmyciPHerKeyS'
const hashKey = crypto.createHash('sha256')
hashKey.update(plainKey)
const key = hashKey.digest() // .digest('hex').substring(0, 32) // Fix 2: no hex encoding
let encrypted = Buffer.from(data.doc, 'base64').toString('hex')
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
decipher.setAutoPadding(false) // Fix 3: disable default PKCS#7 padding
let decrypted = decipher.update(encrypted, 'hex', 'utf-8')
decrypted += decipher.final('utf-8')
console.log(decrypted) // plaintext zero-padded, if necessary remove trailing 0x00 values, e.g. as in the following:
console.log(decrypted.replace(new RegExp("\0+$"), "")) // <eEMPLOYERS ESOF="07-02-2022"><employer pPEN="110474000002" pEmployerName="JOSE A TERAMOTO ODM" pEmployerAddress="ORANBO, PASIG CITY"/></eEMPLOYERS>

How to decrypt a .NET Forms Authentication Cookie in Node.js

I am trying to see if its possible to decrypt a .NET Forms Authentication Cookie in Node.js
The cookie is generated via .NET Framework 4.6
If i check the machineKey value - it uses settings:
decryptionKey="xxxxxxxxxxxxxxxx" validation="SHA1" decryption="DES"
And if i look at the .NET source i believe it defaults to the cbc des cipher:
https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/mscorlib/system/security/cryptography/descryptoserviceprovider.cs#L90
With IV key length of 8 bytes
and hmac size of 20 bytes
In the case of the ASPX cookie it has format:
IV + DATA + HMAC
So if i try something like this below (which when decrypted should be a binary FormsAuthenticationTicket):
const COOKIE_CONTENTS = '86F1EDAAE112A4E56EB1DAA75411F07E8D82F648A87F13E8386735610....REDACTED FULL VALUE';
const decryptionKey = "xxxxxxxxxxxxxxxx";
const algorithm = 'des-cbc';
const key = Buffer.from(decryptionKey, "hex");
let cookie = COOKIE_CONTENTS;
let blob = Buffer.from(cookie, 'hex');
const ivSize = 8;
const hmacSize = 20;
let iv = blob.slice(0, ivSize);
let hmac = blob.slice(blob.length - hmacSize);
let encrypted = blob.slice(ivSize, blob.length - hmacSize);
console.log("Len (cookie):", cookie.length);
console.log("Len (blob):", blob.length);
console.log("IV:", iv, "len:", iv.length);
console.log("HMAC:", hmac, "len:", hmac.length);
console.log("Encrypted:", encrypted, "len:", encrypted.length);
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = Buffer.from(decipher.update(encrypted, 'binary', 'binary') + decipher.final('binary'), 'binary');
And it works!
However when the token is generated with httpRuntime setting:
<httpRuntime targetFramework="4.5" enableVersionHeader="false" maxRequestLength="10240" />
I get a failure
error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
And unfortunately this is the auth tokens i need to decrypt as they are on staging and production systems.
This may be due to the way .NET has changed the way auth is done when you opt into targetFramework="4.5"
It has .NET 4.5 “cryptographic improvements”
See: https://devblogs.microsoft.com/dotnet/cryptographic-improvements-in-asp-net-4-5-pt-2/
I think the rundown is that:
“Purpose” is passed to the crypto routines that describe purpose, we need to provide the same string to decrypt it
Changes to how Message Authentication Code is stored (MAC)
Some of the .NET source code is referenced here: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.Web/Security/Cryptography/Purpose.cs
and also I can see the 4.5 setting block here: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.Web/Security/FormsAuthentication.cs#L155
and the code eventually ends up here:
https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.Web/Security/Cryptography/MachineKeyDataProtectorFactory.cs#L25-L29
but its hard to follow after that. Seems as though 4.5 mode bipasses the DES settings we have passed to it, so who knows what cipher its actually using when in this mode?
Unfortunately I cant just remove the targetFramework="4.5" part as this will mean all user tokens will fail after this is rolled out, meaning all users will need to login again which is not acceptable.
Does anyone know more details on how this can be done with a 4.5 “cryptographic improvements” token?
Any ideas on what I am missing with these crypto settings - it would be great if this can be done in Node.js
UPDATE:
I have tried what #Sebastian has mentioned and tried to port over some of the python code without success (I believe i have written the node code correctly, please let me know if i'm missing something)
eg:
function writeUnsignedInt(v, buf, offset) {
buf.writeInt32BE(v, offset);
}
// conversion of this: https://lowleveldesign.org/2014/11/11/decrypting-asp-net-identity-cookies/
function deriveKey(key, label, context, keyLengthInBits) {
let labelCount = 0;
let contextCount = 0;
if (label) {
labelCount = label.length;
}
if (context) {
contextCount = context.length;
}
const buffer = Buffer.alloc((4 + labelCount + 1 + contextCount + 4));
if (labelCount > 0) {
buffer.write(label, 4, 'ascii');
}
if (contextCount > 0) {
buffer.write(context, 5 + labelCount, 'ascii');
}
writeUnsignedInt(keyLengthInBits, buffer, 5 + labelCount + contextCount);
let destOffset = 0;
let value = parseInt(keyLengthInBits / 8, 10);
let resultBuffer = Buffer.alloc(value);
let num = 1;
while(value > 0) {
writeUnsignedInt(num, buffer, 0);
var hmac = crypto.createHmac('sha512', key);
let bufferString = buffer.toString();
let hashedData = hmac.update(bufferString);
let generatedHmac = hashedData.digest('hex');
let count = Math.min(value, generatedHmac.length);
resultBuffer.write(generatedHmac.substring(0, count), destOffset);
destOffset += count;
value -= count;
num += 1;
}
return resultBuffer.toString();
}
const algorithm = 'des-cbc';
let key = Buffer.from(decryptionKey, "hex");
let blob = Buffer.from(cookie, 'hex');
const ivSize = 8;
const hmacSize = 20;
let iv = blob.slice(0, ivSize);
let hmac = blob.slice(blob.length - hmacSize);
let encrypted = blob.slice(ivSize, blob.length - hmacSize);
let dkey = deriveKey(key, 'FormsAuthentication.Ticket', '>Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware\x11ApplicationCookie\x02v1', 64);
const decipher = crypto.createDecipheriv(algorithm, dkey, iv);
let decrypted = Buffer.from(decipher.update(encrypted, 'binary', 'binary') + decipher.final('binary'), 'binary');
I think the main differences is:
I need to deal with DES and not AES (as thats what has been set in the Web.Config file) with different IV and key sizes.
Im dealing with a forms authentication ticket and not owin auth or anti forgery token.
Im taking a stab at what the "label" is, im setting it as ">Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware\x11ApplicationCookie\x02v1" but not sure thats correct (cant find the source code for this)
Im not sure I need to do any padding / base64 decoding of the encrypted data, because its in hex format (i basically get the DATA part from IV + DATA + HMAC and set it as a blob and pass to decipher.update)

I am trying to convert this crypto encrypt and decrypt js code into typescript for an Angular project

I am using crypto in node.js for encryption and decryption of user credentials, which works fine. On the other hand, I have to use the same approach to getting the exact result of encryption and decryption in Angular. As it's confirmed that crypto is not working in Angular. I would like to know any way to achieve the required result.
I added npm i --save-dev #types/node but this is not working for me. Also added crypto but the module was not found. I also use npm crypto-js encryption is done but not the same as my js method.
This is my js file code, I need to implement the same method in my Angular project:
'use strict';
const crypto = require('crypto');
const IV_LENGTH = 16;
encrypt(text: any, key: any) {
console.log('Before Encrption Data is-->', text, key)
let iv = crypto.randomBytes(this.IV_LENGTH);
let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
let returnable = iv.toString('hex') + ':' + encrypted.toString('hex');
return returnable;
}
decrypt(text: any, key: any) {
let textParts = text.split(':');
let iv = Buffer.from(textParts.shift(), 'hex');
let encryptedText = Buffer.from(textParts.join(':'), 'hex');
let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
When I added crypto this type of error occurred:

"Unsupported state or unable to authenticate data" with aes-128-gcm in Node

I'm trying to implement encrypt/decrypt functions using aes-128-gcm as provided by node crypto. From my understanding, gcm encrypts the ciphertext but also hashes it and provides this as an 'authentication tag'. However, I keep getting the error: "Unsupported state or unable to authenticate data".
I'm not sure if this is an error in my code - looking at the encrypted ciphertext and auth tag, the one being fetched by the decrypt function is the same as the one produced by the encrypt function.
function encrypt(plaintext) {
// IV is being generated for each encryption
var iv = crypto.randomBytes(12),
cipher = crypto.createCipheriv(aes,key,iv),
encryptedData = cipher.update(plaintext),
tag;
// Cipher.final has been called, so no more encryption/updates can take place
encryptedData += cipher.final();
// Auth tag must be generated after cipher.final()
tag = cipher.getAuthTag();
return encryptedData + "$$" + tag.toString('hex') + "$$" + iv.toString('hex');
}
function decrypt(ciphertext) {
var cipherSplit = ciphertext.split("$$"),
text = cipherSplit[0],
tag = Buffer.from(cipherSplit[1], 'hex'),
iv = Buffer.from(cipherSplit[2], 'hex'),
decipher = crypto.createDecipheriv(aes,key,iv);
decipher.setAuthTag(tag);
var decryptedData = decipher.update(text);
decryptedData += decipher.final();
}
The error is being thrown by decipher.final().
In case if someone still tries to get a working example of encryption and decryption process.
I've left some comments that should be taken into consideration.
import * as crypto from 'crypto';
const textToEncode = 'some secret text'; // utf-8
const algo = 'aes-128-gcm';
// Key bytes length depends on algorithm being used:
// 'aes-128-gcm' = 16 bytes
// 'aes-192-gcm' = 24 bytes
// 'aes-256-gcm' = 32 bytes
const key = crypto.randomBytes(16);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algo, key, iv);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(textToEncode, 'utf-8')),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
console.info('Value encrypted', {
valueToEncrypt: textToEncode,
encryptedValue: encrypted.toString('hex'),
authTag: authTag.toString('hex'),
});
// It's important to use the same authTag and IV that were used during encoding
const decipher = crypto.createDecipheriv(algo, key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]);
console.info('Value decrypted', {
valueToDecrypt: encrypted.toString('hex'),
decryptedValue: decrypted.toString('utf-8'),
});
I managed to fix this: the issue was that I wasn't specifying an encoding type for cipher.final() and I was returning it within a String, so it wasn't returning a Buffer object, which decipher.final() was expecting.
To fix, I add 'utf-8' to 'hex' encoding parameters within my cipher.update and cipher.final, and vice versa in decipher.
Edited to add code example - note this is from 2018, so may be outdated now.
function encrypt(plaintext) {
// IV is being generated for each encryption
var iv = crypto.randomBytes(12),
cipher = crypto.createCipheriv(aes,key,iv),
encryptedData = cipher.update(plaintext, 'utf-8', 'hex'),
tag;
// Cipher.final has been called, so no more encryption/updates can take place
encryptedData += cipher.final('hex');
// Auth tag must be generated after cipher.final()
tag = cipher.getAuthTag();
return encryptedData + "$$" + tag.toString('hex') + "$$" + iv.toString('hex');
}
function decrypt(ciphertext) {
var cipherSplit = ciphertext.split("$$"),
text = cipherSplit[0],
tag = Buffer.from(cipherSplit[1], 'hex'),
iv = Buffer.from(cipherSplit[2], 'hex'),
decipher = crypto.createDecipheriv(aes, key, iv);
decipher.setAuthTag(tag);
var decryptedData = decipher.update(text, 'hex', 'utf-8');
decryptedData += decipher.final('utf-8');
}

Resources