How to encrypt and decrypt a number using same key? - node.js

I need a way to encrypt a number (say 3423234234) using a secret key, which can also be decrypted using that same key.
const encrypted = encrypt(number, key)
const decrypted = decrypt(encrypted, key)
I tried CryptoJS using AES, DES, Rabbit and RC4 algorithms, but it gives me a long encrypted value with special characters.
I want some encrypted value like MongoDB's ObjectId (which contains only alphanumeric characters like 1575866cab3f22f0c8510451f293f405, and should not exceed 20 or 40 characters).

I figured out a way.
const decrypt = function (num) {
const base = "tGVwFAPiJqh3maNHdZfby9eIQXcr2zvgD57YMn8KWCxkSUujpB10sL6oRTl4OE"
num = num.toString()
let lastChar, baseConverted = 0, index=0;
while(num !== ''){
lastChar = num.slice(-1);
baseConverted += base.indexOf(lastChar) * Math.pow(62,index);
index++;
num = num.slice(0, num.length-1);
}
return baseConverted;
};
const encrypt = function (num) {
const base = "tGVwFAPiJqh3maNHdZfby9eIQXcr2zvgD57YMn8KWCxkSUujpB10sL6oRTl4OE"
// console.log(base.length)
let mod, baseConverted = '';
while(num){
mod = num%62;
baseConverted = base.slice(mod, mod+1)+baseConverted;
num = Math.floor(num/62);
}
return baseConverted;
};
module.exports = {
encrypt,
decrypt
}
The reason I wanted it because I want to encrypt all the id's of a resource before sharing it with client.
It will decrypt the keys when request is received.

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.

Nodejs AES-256-GCM decrypt the encrypted client message by webcrypto api

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.

Node.js: Getting Invalid RSAES-OAEP padding when trying to decrypt an encrypted symmetric key

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));

Getting error of Invalid IV Length while using aes-256-cbc for encryption in node

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;
}

"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