I have the following code that signs some data in a .js script:
const { RSA_PKCS1_PSS_PADDING } = require('constants');
const crypto = require('crypto');
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
const fs = require('fs');
const keys = fs.createWriteStream('keys.txt');
keys.write(`${publicKey}\n`);
keys.write(`${privateKey}\n`);
function signature(verifyData) {
return crypto.createSign('sha256').sign({
keyLike: Buffer.from(verifyData),
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
}).toString('base64');
}
The script will create a txt file with my public and private keys, such as follows:
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
I tried several ways to generate the same hash as the .js script for the same input and no success. It also cannot verify any hashes created by the .js script. Below are my implementations:
private RsaKeyParameters readPrivateKey(string privateKeyFileName)
{
RsaKeyParameters keyPair;
using (var reader = File.OpenText(privateKeyFileName))
keyPair = (RsaKeyParameters)new PemReader(reader).ReadObject();
return keyPair;
}
bool VerifyDataBouncyCastle(string bodyData, string signature)
{
var data = bodyData;
var signatureBytes = Convert.FromBase64String(signature);
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
signer.Init(false, readPrivateKey($"{DiretorioBase}\\public.txt"));
signer.BlockUpdate(Encoding.UTF8.GetBytes(data), 0, data.Length);
var success = signer.VerifySignature(signatureBytes);
return success;
}
string SignDataBouncyCastle(string data)
{
// Verify using the public key
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
signer.Init(true, readPrivateKey($"{DiretorioBase}\\private.txt"));
signer.BlockUpdate(Encoding.UTF8.GetBytes(data), 0, data.Length);
return Convert.ToBase64String(signer.GenerateSignature());
}
public byte[] SignDataNetCore(byte[] data)
{
// privateKey does not have the ---BEGIN and ---END headers.
var privateKey = File.ReadAllText($"{DiretorioBase}\\private.txt");
var rsaPrivateKey = RSA.Create();
rsaPrivateKey.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
return rsaPrivateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
public bool VerifyDataNetCore(byte[] data, byte[] signature)
{
var publicKey = File.ReadAllText($"{DiretorioBase}\\public.txt");
var rsaPublicKey = RSA.Create();
rsaPublicKey.ImportFromPem(publicKey);
return rsaPublicKey.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
None of the above methods will produce the same signature using the same input and same key generated by the .js script.
What am I missing?
--Edit--
I changed the .js signature method like this:
function signature(verifyData) {
var cSign = crypto.createSign('sha256');
cSign.update(verifyData);
return cSign.sign({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
});
}
And the C# verified code to this:
bool isVerified()
{
string x509Pem = #"-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----";
byte[] message = Encoding.UTF8.GetBytes(validar);
byte[] signature = Convert.FromBase64String(hash64);
PemReader pr = new PemReader(new StringReader(x509Pem));
AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();
RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKey);
RSACng rsaCng = new RSACng();
rsaCng.ImportParameters(rsaParams);
bool verified = rsaCng.VerifyData(message, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
return verified;
}
It still returns false.
PSS has a number of parameters, including the salt length. RFC8017, A.2.3. RSASSA-PSS defines a default salt length that corresponds to the output length of the digest, i.e. 32 bytes for SHA256.
Your recent C# code applies the C# built-in classes that use this default salt length. A different salt length cannot be specified!
The NodeJS code, on the other hand, defaults to the maximum possible salt length (crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN), which is given by:
<keysize> - <digest output length> - 2 = 256 - 32 - 2 = 222.
Thus, the two codes are incompatible!
Unlike the C# built-in classes, BouncyCastle allows the salt length to be configured:
string x509Pem = #"-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----";
string validar = "...";
string hash64 = "...";
byte[] message = Encoding.UTF8.GetBytes(validar);
byte[] signature = Convert.FromBase64String(hash64);
PemReader pr = new PemReader(new StringReader(x509Pem));
AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();
PssSigner pssSigner = new PssSigner(new RsaEngine(), new Sha256Digest(), 256 - 32 - 2);
pssSigner.Init(false, publicKey);
pssSigner.BlockUpdate(message, 0, message.Length);
bool valid = pssSigner.VerifySignature(signature); // succeeds when the maximum possible salt length is used
With this, verification is successful.
Note that in the NodeJS code you can explicitly change the salt length to the output length of the digest (crypto.constants.RSA_PSS_SALTLEN_DIGEST). Then verification will also work with the built-in C# classes.
Related
We are using the services of a third party in our organization in which we have to send some data to them in an encrypted manner. Recently, they updated the encryption algorithm to AES/GCM/NoPadding.
They have their code in java whereas we use javascript. they have shared with us their implementation of the algorithm in Java which we have to replicate and implement in JS because that is what we use.
I am facing challenges in converting this code. Attaching both Java implementation which works like a charm and the JS code which is not working as expected. Although I have tried multiple things but none of them worked for me. So, I am sharing only the latest code that I tried.
I have no knowledge of Java or cryptography so any help in that direction will be highly appreciated.
JAVA Code -
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Encryption class for managing all types of AES encryptions
*/
public class EncryptionUtil {
private final Builder mBuilder;
private final static String HEX = "0123456789ABCDEF";
private EncryptionUtil(Builder builder) {
mBuilder = builder;
}
public static EncryptionUtil getDefault(String key, String salt, byte[] iv) {
try {
return Builder.getDefaultBuilder(key, salt, iv).build();
} catch (NoSuchAlgorithmException e) {
return null;
}
}
public String encryptOrNull(String data) {
try {
return encrypt(data);
} catch (Exception e) {
return "";
}
}
private String encrypt(String data) throws Exception {
if (data == null) return null;
SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey()));
return doEncryptAES(data, secretKey, mBuilder.getAlgorithm(), mBuilder.getCharsetName());
}
private String decrypt(String data) throws Exception {
if (data == null) return null;
SecretKey secretKey = getSecretKey(hashTheKey(mBuilder.getKey()));
return doDecryptAES(data, secretKey, mBuilder.getAlgorithm());
}
private String doEncryptAES(String inputString,
SecretKey key, String xForm, String charset) throws Exception {
byte inpBytes[] = inputString.getBytes(charset);
Cipher cipher = Cipher.getInstance(xForm);
switch (xForm) {
case "AES/ECB/PKCS5Padding":
case "AES/ECB/NoPadding":
cipher.init(Cipher.ENCRYPT_MODE, key);
break;
case "AES/CBC/PKCS5Padding":
case "AES/CBC/NoPadding":
cipher.init(Cipher.ENCRYPT_MODE, key, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom());
break;
case "AES/GCM/NoPadding":
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, mBuilder.getIv()));
byte[] encryptedData = cipher.doFinal(inpBytes);
ByteBuffer byteBuffer = ByteBuffer.allocate(4 + mBuilder.getIv().length + encryptedData.length);
byteBuffer.putInt(mBuilder.getIv().length);
byteBuffer.put(mBuilder.getIv());
byteBuffer.put(encryptedData);
return toHex(byteBuffer.array());
}
return toHex(cipher.doFinal(inpBytes));
}
/**
* for AES in GCM mode kitkat version is required
*
* #param inputString is String we want to decrypt
* #param key is symmetric key use for decryption and it similar to key used for encryption (128,192,256)
* #param xForm is the transformation form in which form we want to transform
* (AES/ECB/PKCS5Padding,AES/ECB/NoPadding,AES/CBC/PKCS5Padding,AES/CBC/NoPadding,AES/GCM/NoPadding)
* #return it reurn decrypted string
* #throws Exception NOSuchAlgorithmEXception,NoSuchPaddingEXception
*/
private String doDecryptAES(String inputString,
SecretKey key, String xForm) throws Exception {
byte[] inpBytes = toByte(inputString);
Cipher cipher = Cipher.getInstance(xForm);
switch (xForm) {
case "AES/ECB/PKCS5Padding":
case "AES/ECB/NoPadding":
cipher.init(Cipher.DECRYPT_MODE, key);
break;
case "AES/CBC/PKCS5Padding":
case "AES/CBC/NoPadding":
cipher.init(Cipher.DECRYPT_MODE, key, mBuilder.getIvParameterSpec(), mBuilder.getSecureRandom());
break;
case "AES/GCM/NoPadding":
ByteBuffer byteBuffer = ByteBuffer.wrap(inpBytes);
int noonceSize = byteBuffer.getInt();
if (noonceSize < 12 || noonceSize >= 16)
throw new IllegalArgumentException("Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file.");
byte[] iv = new byte[noonceSize];
byteBuffer.get(iv);
byte[] cipherBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(cipherBytes);
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
return new String(cipher.doFinal(cipherBytes), mBuilder.getCharsetName());
}
return new String(cipher.doFinal(inpBytes), mBuilder.getCharsetName());
}
public String decryptOrNull(String data) {
try {
return decrypt(data);
} catch (Exception e) {
return "";
}
}
private SecretKey getSecretKey(char[] key) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance(mBuilder.getSecretKeyType());
KeySpec spec = new PBEKeySpec(key, mBuilder.getSalt().getBytes(mBuilder.getCharsetName()), mBuilder.getIterationCount(), mBuilder.getKeyLength());
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), mBuilder.getKeyAlgorithm());
}
private char[] hashTheKey(String key) throws UnsupportedEncodingException, NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance(mBuilder.getDigestAlgorithm());
messageDigest.update(key.getBytes(mBuilder.getCharsetName()));
return toHex(messageDigest.digest()).toCharArray();
}
private byte[] toByte(String hexString) {
int len = hexString.length() / 2;
byte[] result = new byte[len];
for (int i = 0; i < len; i++)
result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue();
return result;
}
public String toHex(byte[] stringBytes) {
StringBuffer result = new StringBuffer(2 * stringBytes.length);
for (int i = 0; i < stringBytes.length; i++) {
result.append(HEX.charAt((stringBytes[i] >> 4) & 0x0f)).append(HEX.charAt(stringBytes[i] & 0x0f));
}
return result.toString();
}
private static class Builder {
private byte[] mIv;
private int mKeyLength;
private int mIterationCount;
private String mSalt;
private String mKey;
private String mAlgorithm;
private String mKeyAlgorithm;
private String mCharsetName;
private String mSecretKeyType;
private String mDigestAlgorithm;
private String mSecureRandomAlgorithm;
private SecureRandom mSecureRandom;
private IvParameterSpec mIvParameterSpec;
static Builder getDefaultBuilder(String key, String salt, byte[] iv) {
return new Builder()
.setIv(iv)
.setKey(key)
.setSalt(salt)
.setKeyLength(128)
.setKeyAlgorithm("AES")
.setCharsetName("UTF8")
.setIterationCount(1)
.setDigestAlgorithm("SHA-256")
.setAlgorithm("AES/GCM/NoPadding")
.setSecureRandomAlgorithm("SHA1PRNG")
.setSecretKeyType("PBKDF2WithHmacSHA1");
}
private EncryptionUtil build() throws NoSuchAlgorithmException {
setSecureRandom(SecureRandom.getInstance(getSecureRandomAlgorithm()));
SecureRandom secureRandom = new SecureRandom();
byte[] iv = getIv();
secureRandom.nextBytes(iv);
setIvParameterSpec(new IvParameterSpec(iv));
return new EncryptionUtil(this);
}
private String getCharsetName() {
return mCharsetName;
}
private Builder setCharsetName(String charsetName) {
mCharsetName = charsetName;
return this;
}
private String getAlgorithm() {
return mAlgorithm;
}
private Builder setAlgorithm(String algorithm) {
mAlgorithm = algorithm;
return this;
}
private String getKeyAlgorithm() {
return mKeyAlgorithm;
}
private Builder setKeyAlgorithm(String keyAlgorithm) {
mKeyAlgorithm = keyAlgorithm;
return this;
}
private String getSecretKeyType() {
return mSecretKeyType;
}
private Builder setSecretKeyType(String secretKeyType) {
mSecretKeyType = secretKeyType;
return this;
}
private String getSalt() {
return mSalt;
}
private Builder setSalt(String salt) {
mSalt = salt;
return this;
}
private String getKey() {
return mKey;
}
private Builder setKey(String key) {
mKey = key;
return this;
}
private int getKeyLength() {
return mKeyLength;
}
Builder setKeyLength(int keyLength) {
mKeyLength = keyLength;
return this;
}
private int getIterationCount() {
return mIterationCount;
}
Builder setIterationCount(int iterationCount) {
mIterationCount = iterationCount;
return this;
}
private String getSecureRandomAlgorithm() {
return mSecureRandomAlgorithm;
}
Builder setSecureRandomAlgorithm(String secureRandomAlgorithm) {
mSecureRandomAlgorithm = secureRandomAlgorithm;
return this;
}
private byte[] getIv() {
return mIv;
}
Builder setIv(byte[] iv) {
mIv = iv;
return this;
}
private SecureRandom getSecureRandom() {
return mSecureRandom;
}
Builder setSecureRandom(SecureRandom secureRandom) {
mSecureRandom = secureRandom;
return this;
}
private IvParameterSpec getIvParameterSpec() {
return mIvParameterSpec;
}
Builder setIvParameterSpec(IvParameterSpec ivParameterSpec) {
mIvParameterSpec = ivParameterSpec;
return this;
}
private String getDigestAlgorithm() {
return mDigestAlgorithm;
}
Builder setDigestAlgorithm(String digestAlgorithm) {
mDigestAlgorithm = digestAlgorithm;
return this;
}
}
public static void main(String[] args) {
String secretKey = "some_secret_key";
String salt = "some_secret_salt";
EncryptionUtil encryptionUtil = EncryptionUtil.getDefault(secretKey, salt, new byte[12]);
String data = "Data to encrypt";
System.out.println("Encrypted:");
String encrypted = encryptionUtil.encryptOrNull(data);
System.out.println(encrypted);
System.out.println("Decrypted:");
System.out.println(encryptionUtil.decryptOrNull(encrypted));
}
}
Please note I need help only to encrypt the data
JS Code -
import * as crypto from 'crypto';
export const encData = () => {
const data = 'Data to encrypt';
const secretKey = 'some_secret_key';
const salt = 'some_secret_salt';
let key = '';
const keyHash = key => {
const hash = crypto.createHash('sha256');
const hashedKey = hash.update(key, 'utf-8');
return hashedKey.digest('hex').toUpperCase();
};
const getSecretKey = key => {
return crypto.pbkdf2Sync(key, salt, 1, 16, 'sha1');
};
key = getSecretKey(keyHash(secretKey));
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-128-gcm', key, iv);
const buffer = Buffer.from(_.isPlainObject(data) ? JSON.stringify(data) : data);
// Updating text
let encrypted = cipher.update(buffer);
// Using concatenation
encrypted = Buffer.concat([encrypted, cipher.final()]);
return encrypted.toString('base64');
};
console.log(encData());
To make sure my code is working fine I am decrypting my encoded string generated with JS function by passing it to Java decrypt function.
In the Java code, the result of the encryption is composed as follows:
iv-length (4 bytes, BE) | IV | ciphertext | authentication tag
In contrast, in the NodeJS code the result consists only of the ciphertext, i.e. IV length, IV and tag are missing and must be added.
Here it must be taken into account that Java's SunJCE provider automatically concatenates ciphertext and tag, while this must happen explicitly in the NodeJS code.
Also, the ciphertext is returned hex encoded in the Java code, while it is Base64 encoded in the NodeJS code. This also needs to be changed in the NodeJS code.
The fix is to replace in the NodeJS code the lines:
// Using concatenation
encrypted = Buffer.concat([encrypted, cipher.final()]);
return encrypted.toString('base64');
with:
const length = Buffer.allocUnsafe(4);
length.writeUInt32BE(iv.length);
// Using concatenation
encrypted = Buffer.concat([length, iv, encrypted, cipher.final(), cipher.getAuthTag()]);
return encrypted.toString('hex');
With this, the NodeJS code returns a result that can be decrypted by the Java code.
Note that a static salt is insecure. Instead, the salt should be randomly generated like the IV for each encryption and passed along with the ciphertext.
Also, an iteration count of 1 is not secure, the value should be as high as possible with acceptable performance.
Hashing the key with SHA256 before the PBKDF2 derivation is actually not necessary (at least if PBKDF2 is applied correctly).
I have written the following Java code to encrypt a message/data. Currently it is using default encryption algorithm (AES/ECB/PKCS5PADDING). In JavaScript while decrypting I have used mode ECB. I read articles that ECB is not secure. So I need to move to CBC mode. But changing the mode is causing issue for me. Can you help me to change the mode in proper way so that it is compatible?
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class EncryptDecryptImpl {
private static final String secretKey = "abcdefghijklmnop";
private static final String mySecretKey = "my-secret-key";
private static final String encryptionAlgorithm = "AES"; // need to use AES/CBC/PKCS5Padding
public static String encrypt(String data, String secret) {
try {
Key key = generateKey(secret);
Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedValue = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedValue);
} catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException |
BadPaddingException | IllegalBlockSizeException e) {
e.printStackTrace();
}
return null;
}
public static String decrypt(String strToDecrypt, String secret) {
try {
Key key = generateKey(secret);
Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
cipher.init(Cipher.DECRYPT_MODE, key);
return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)));
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException |
BadPaddingException | IllegalBlockSizeException e) {
e.printStackTrace();
}
return null;
}
private static Key generateKey(String secret) {
byte[] decoded = Base64.getDecoder().decode(secret.getBytes());
return new SecretKeySpec(decoded, encryptionAlgorithm);
}
public static String encodeKey(String key) {
byte[] encoded = Base64.getEncoder().encode(key.getBytes());
return new String(encoded);
}
public static String decodeKey(String key) {
byte[] decoded = Base64.getDecoder().decode(key.getBytes());
return new String(decoded);
}
public static String encodedBase64Key() {
return encodeKey(secretKey);
}
public static String decodedBase64Key(String encryptedSecretKey) {
return decodeKey(encryptedSecretKey);
}
public static String aesEncryptedSecretKey() {
return EncryptDecryptImpl.encrypt(mySecretKey, encodedBase64Key());
}
public static String aesDecryptedSecretKey() {
return EncryptDecryptImpl.decrypt(aesEncryptedSecretKey(), encodedBase64Key());
}
}
Test:
String encryptedSecretKey = EncryptDecryptImpl.aesEncryptedSecretKey(); // cipher text
JavaScript:
export const getSecretKey = () => {
const encryptedBase64Key = 'bXVzdEJlMTZCeXRlc0tleQ==';
const parsedBase64Key = enc.Base64.parse(encryptedBase64Key);
const encryptedCipherText = getSessionStorageItem('uselessKey');
let decryptedData = '';
if (encryptedCipherText !== null) {
decryptedData = AES.decrypt(encryptedCipherText, parsedBase64Key, {
mode: mode.ECB, // need to use CBC
padding: pad.Pkcs7
})
}
return decryptedData.toString(enc.Utf8).toString();
}
The roadmap has already been roughly outlined by M. Fehr in his comment. The CBC mode uses an IV. In general it has to be considered that a key/IV pair must not be applied more than once for security reasons. Therefore, the IV is usually randomly generated for each encryption.
The IV must be known during decryption. Hence, it is passed together with the ciphertext. However, since the IV is not secret, it is passed unencrypted, usually concatenated with the ciphertext in the order IV | ciphertext.
For this additional functionality the encrypt() method in the Java code has to be adapted as follows (for simplicity without exception handling):
import java.security.SecureRandom;
import javax.crypto.spec.IvParameterSpec;
import java.nio.ByteBuffer;
...
private static final String encryptionAlgorithm = "AES/CBC/PKCS5Padding";
private static final String keyAlgorithm = "AES";
...
public static String encrypt(String data, String secret) {
Key key = generateKey(secret);
Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
// Generate random IV, encrypt and concatenate IV and ciphertext
SecureRandom secureRandom = new SecureRandom();
byte[] iv = new byte[16];
secureRandom.nextBytes(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] ciphertext = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
byte[] encryptedValue = ByteBuffer.allocate(iv.length + ciphertext.length).put(iv).put(ciphertext).array();
return Base64.getEncoder().encodeToString(encryptedValue);
}
Note that in generateKey() with this change, keyAlgorithm must be used instead of encryptionAlgorithm.
On the JavaScript side, IV and ciphertext must be separated. CBC and PKCS7 are the default and do not need to be specified explicitly.
The ciphertext in the following CryptoJS code was generated with the above C# code and returns the original plaintext:
const enc = CryptoJS.enc, lib = CryptoJS.lib, AES = CryptoJS.AES;
const encryptedBase64Key = 'bXVzdEJlMTZCeXRlc0tleQ==';
const parsedBase64Key = enc.Base64.parse(encryptedBase64Key);
const encryptedCipherText = 'FZ+lnxu9iZGkxmmBxae32ToSkoi+a2/BpzAd6HYnyBjFjCmpssVUVx9N+KQbhpU2ALpJVG8my25KTG6xg7AOXQ==';
const parsedCipherText = enc.Base64.parse(encryptedCipherText);
const iv = lib.WordArray.create(parsedCipherText.words.slice(0, 16 / 4));
const ciphertext = lib.WordArray.create(parsedCipherText.words.slice(16 / 4));
if (encryptedCipherText !== null) {
decryptedData = AES.decrypt({ciphertext: ciphertext}, parsedBase64Key,{iv: iv});
}
console.log(decryptedData.toString(CryptoJS.enc.Utf8));
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
Decryption in the Java code and encryption in the JavaScript code are to be changed analogously.
Note that old data must be migrated, because data encrypted with ECB mode cannot be decrypted with CBC mode.
I need to decrypt a hex message in JavaScript that has the exact same outcome as the code written in Java. However the Javascript version using CryptoJs returns an empty result
Code in Java:
private static void create()
{
byte[] sessionKey = fromHexString("dae25b4defd646cd99b7b95d450d6646");
byte[] data = fromHexString("2700012e27999bdaa6b0530375be269985a0238e5e4baf1528ebaf34a8e5e8c13a58b25bcb82514ee6c86c02ff77ac52bdbd88");
byte[] payload_data = new byte[48];
byte[] decrypted_data = new byte[48];
for(int i=0;i<48;i++) {
payload_data[i]= data[3+i];
}
try{
SecretKeySpec skeySpec = new SecretKeySpec(sessionKey, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
decrypted_data = cipher.doFinal(payload_data);
}catch(Exception e){
System.out.println(e);
}
String my_data = byteArrayToHex(decrypted_data);
System.out.println(my_data);
}
private static String byteArrayToHex(byte[] a) {
StringBuilder sb = new StringBuilder(a.length * 2);
for(byte b: a)
sb.append(String.format("%02X", b));
return sb.toString();
}
private static byte[] fromHexString(String src) {
byte[] biBytes = new BigInteger("10" + src.replaceAll("\\s", ""), 16).toByteArray();
return Arrays.copyOfRange(biBytes, 1, biBytes.length);
}
which returns a result of: "248A8143837F51E03C3522934DD47C38612C90EC57D79D7DE6174EAC85B75F9ADCD7D6686EBF4B9F2E9FE441D373E69E"
Code in JavaScript:
import * as cryptojs from 'crypto-js';
export function create() {
const sessionKey = Buffer.from('dae25b4defd646cd99b7b95d450d6646', 'hex');
const data = Buffer.from('2700012e27999bdaa6b0530375be269985a0238e5e4baf1528ebaf34a8e5e8c13a58b25bcb82514ee6c86c02ff77ac52bdbd88', 'hex');
const payloadData = Buffer.alloc(48);
for (let i = 0; i < 48; i += 1) {
payloadData[i] = data[3 + i];
}
const decrypted = cryptojs.AES.decrypt(
cryptojs.enc.Hex.parse(toHexString(payloadData)),
cryptojs.enc.Hex.parse(toHexString(sessionKey)),
{
mode: cryptojs.mode.ECB,
padding: cryptojs.pad.NoPadding,
}
).toString(cryptojs.enc.Hex);
console.log({
decrypted,
});
}
function toHexString(byteArray) {
// eslint-disable-next-line no-bitwise
return Array.prototype.map.call(byteArray, byte => `0${(byte & 0xff).toString(16)}`.slice(-2)).join('');
}
result:
{ decrypted: '' }
Any idea on what might be wrong ?
The decryption with CryptoJS could look as follows:
function decrypt() {
var sessionKey = 'dae25b4defd646cd99b7b95d450d6646';
var data = '2700012e27999bdaa6b0530375be269985a0238e5e4baf1528ebaf34a8e5e8c13a58b25bcb82514ee6c86c02ff77ac52bdbd88';
var payload_data = data.substr(6);
var decrypted = CryptoJS.AES.decrypt(
payload_data,
CryptoJS.enc.Hex.parse(sessionKey),
{
format: CryptoJS.format.Hex,
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding,
}
).toString(CryptoJS.enc.Hex);
console.log(decrypted.replace(/(.{48})/g,'$1\n'));
}
decrypt();
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
Update: Regarding your comment: The issue in your code is that the ciphertext is passed as WordArray. The following two changes are one way to make it work:
...
const decrypted = cryptojs.AES.decrypt(
toHexString(payloadData), // pass the ciphertext as hex encoded string
cryptojs.enc.Hex.parse(toHexString(sessionKey)),
{
format: cryptojs.format.Hex, // specify the encoding of the ciphertext
mode: cryptojs.mode.ECB,
...
cryptojs.AES.decrypt() expects the ciphertext in a CipherParams object (and not simply in a WordArray). Alternatively the ciphertext can be passed Base64 encoded or in another encoding that must be specified explicitly with the format parameter (e.g. hexadecimal here, Base64 is the default). The ciphertext is then implicitly converted into a CipherParams object, see here.
But please consider: Since all conversions can be done with CryptoJS onboard means, helpers like toHexString() are not really necessary. For this there are especially the encoder classes, see here. The same applies to the NodeJS Buffer. It makes more sense to work with WordArrays, because they are processed directly by CryptoJS.
Our project is using asymmetric encryption with nacl.box and ephemeral keys:
encrypt(pubKey, msg) {
if (typeof msg !== 'string') {
msg = JSON.stringify(msg)
}
let ephemKeys = nacl.box.keyPair()
let msgArr = nacl.util.decodeUTF8(msg)
let nonce = nacl.randomBytes(nacl.box.nonceLength)
p(`naclRsa.pubKey=${this.pubKey}`)
let encrypted = nacl.box(
msgArr,
nonce,
nacl.util.decodeBase64(pubKey),
ephemKeys.secretKey
)
let nonce64 = nacl.util.encodeBase64(nonce)
let pubKey64 = nacl.util.encodeBase64(ephemKeys.publicKey)
let encrypted64 = nacl.util.encodeBase64(encrypted)
return {nonce: nonce64, ephemPubKey: pubKey64, encrypted: encrypted64}
}
We presently have node.js apps that then decrypt these messages. We would like the option to use jvm languages for some features. There does not seem to be the richness of established players for tweet-nacl on the jvm but it seems
tweetnacl-java https://github.com/InstantWebP2P/tweetnacl-java
and its recommended implementation
° tweetnacl-fast https://github.com/InstantWebP2P/tweetnacl-java/blob/master/src/main/java/com/iwebpp/crypto/TweetNaclFast.java
were a popular one.
It is unclear what the analog to the asymmetric encryption with ephemeral keys were in that library. Is it supported? Note that I would be open to either java or kotlin if this were not supported in tweetnacl-java.
tweetnacl-java is a port of tweetnacl-js. It is therefore to be expected that both provide the same functionality. At least for the posted method this is the case, which can be implemented on the Java side with TweetNaclFast as follows:
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.iwebpp.crypto.TweetNaclFast;
import com.iwebpp.crypto.TweetNaclFast.Box;
import com.iwebpp.crypto.TweetNaclFast.Box.KeyPair;
...
private static EncryptedData encrypt(byte[] pubKey, String msg) {
KeyPair ephemKeys = Box.keyPair();
byte[] msgArr = msg.getBytes(StandardCharsets.UTF_8);
byte[] nonce = TweetNaclFast.randombytes(Box.nonceLength);
Box box = new Box(pubKey, ephemKeys.getSecretKey());
byte[] encrypted = box.box(msgArr, nonce);
String nonce64 = Base64.getEncoder().encodeToString(nonce);
String ephemPubKey64 = Base64.getEncoder().encodeToString(ephemKeys.getPublicKey());
String encrypted64 = Base64.getEncoder().encodeToString(encrypted);
return new EncryptedData(nonce64, ephemPubKey64, encrypted64);
}
...
class EncryptedData {
public EncryptedData(String nonce, String ephemPubKey, String encrypted) {
this.nonce = nonce;
this.ephemPubKey = ephemPubKey;
this.encrypted = encrypted;
}
public String nonce;
public String ephemPubKey;
public String encrypted;
}
In order to demonstrate that both sides are compatible, in the following a plaintext is encrypted on the Java side and decrypted on the JavaScript side:
First, a key pair is needed on the JavaScript side, whose public key (publicKeyJS) is passed to the Java side. The key pair on the JavaScript side can be generated as follows:
let keysJS = nacl.box.keyPair();
let secretKeyJS = keysJS.secretKey;
let publicKeyJS = keysJS.publicKey;
console.log("Secret key: " + nacl.util.encodeBase64(secretKeyJS));
console.log("Public key: " + nacl.util.encodeBase64(publicKeyJS));
with the following sample output:
Secret key: YTxAFmYGm4yV2OP94E4pcD6LSsN4gcSBBAlU105l7hw=
Public key: BDXNKDHeq0vILm8oawAGAQtdIsgwethzBTBqmsWI+R8=
The encryption on the Java side is then using the encrypt method posted above (and publicKeyJS):
byte[] publicKeyJS = Base64.getDecoder().decode("BDXNKDHeq0vILm8oawAGAQtdIsgwethzBTBqmsWI+R8=");
EncryptedData encryptedFromJava = encrypt(publicKeyJS, "I've got a feeling we're not in Kansas anymore...");
System.out.println("Nonce: " + encryptedFromJava.nonce);
System.out.println("Ephemeral public key: " + encryptedFromJava.ephemPubKey);
System.out.println("Ciphertext: " + encryptedFromJava.encrypted);
with the following sample output:
Nonce: FcdzXfYwSbI0nq2WXsLe9aAh94vXSoWd
Ephemeral public key: Mde+9metwF1jIEij5rlZDHjAStR/pd4BN9p5JbZleSg=
Ciphertext: hHo7caCxTU+hghcFZFv+djAkSlWKnC12xj82V2R/Iz9GdOMoTzjoCDcz9m/KbRN6i5dkYi3+Gf0YTtKlZQWFooo=
The decryption on the JS side gives the original plaintext (using secretKeyJS):
let nonce = "FcdzXfYwSbI0nq2WXsLe9aAh94vXSoWd";
let ephemPubKey = "Mde+9metwF1jIEij5rlZDHjAStR/pd4BN9p5JbZleSg=";
let encrypted = "hHo7caCxTU+hghcFZFv+djAkSlWKnC12xj82V2R/Iz9GdOMoTzjoCDcz9m/KbRN6i5dkYi3+Gf0YTtKlZQWFooo=";
let secretKeyJS = nacl.util.decodeBase64("YTxAFmYGm4yV2OP94E4pcD6LSsN4gcSBBAlU105l7hw=");
let decryptedFromJS = decrypt(secretKeyJS, {nonce: nonce, ephemPubKey: ephemPubKey, encrypted: encrypted});
console.log(nacl.util.encodeUTF8(decryptedFromJS)); // I've got a feeling we're not in Kansas anymore...
function decrypt(secretKey, ciphertext){
let decrypted = nacl.box.open(
nacl.util.decodeBase64(ciphertext.encrypted),
nacl.util.decodeBase64(ciphertext.nonce),
nacl.util.decodeBase64(ciphertext.ephemPubKey),
secretKey
);
return decrypted;
}
<script src="https://cdn.jsdelivr.net/npm/tweetnacl-util#0.15.1/nacl-util.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tweetnacl#1.0.3/nacl.min.js"></script>
My complete code for tweetnacl-java (Kudos to #topaco)
I generated two random key-pairs and saved their secret keys in the application.properties file, so that, i will always have the same pub&sec along with the nonce.
KeyPair baseKeyPair= Box.keyPair();
String baseKeyPairSecretKey = Base64.getEncoder().encodeToString(baseKeyPair.getSecretKey());
KeyPair ephemeralKeyPair= Box.keyPair();
String ephemeralKeyPairSecretKey = Base64.getEncoder().encodeToString(ephemeralKeyPair.getSecretKey());
byte[] nonce = TweetNaclFast.randombytes(Box.nonceLength);
String nonce64 = Base64.getEncoder().encodeToString(nonce);
private final AppConfig config; //you can autowire the config class
private TweetNaclFast.Box.KeyPair getBaseKeyPair() {
byte[] secretKey = Base64.getDecoder().decode(config.getTweetNACLConfig().getBaseSecretKey());
return TweetNaclFast.Box.keyPair_fromSecretKey(mySecretKey);
}
private TweetNaclFast.Box.KeyPair getEphemeralKeyPair() {
byte[] secretKey = Base64.getDecoder().decode(config.getTweetNACLConfig().getEphemeralSecretKey());
return TweetNaclFast.Box.keyPair_fromSecretKey(mySecretKey);
}
private byte[] getNonce() {
return Base64.getDecoder().decode(config.getTweetNACLConfig().getNonce().getBytes(StandardCharsets.UTF_8));
}
public String encrypt(String msg) {
TweetNaclFast.Box.KeyPair baseKeyPair = getBaseKeyPair();
TweetNaclFast.Box.KeyPair ephemeralKeyPair = getEphemeralKeyPair();
byte[] msgArr = msg.getBytes(StandardCharsets.UTF_8);
byte[] nonce = getNonce();
TweetNaclFast.Box box = new TweetNaclFast.Box(baseKeyPair.getPublicKey(), ephemeralKeyPair.getSecretKey());
byte[] encryptedData = box.box(msgArr, nonce);
return Base64.getEncoder().encodeToString(encryptData);
}
public String decrypt(String encryptedData) {
TweetNaclFast.Box.KeyPair baseKeyPair = getBaseKeyPair();
TweetNaclFast.Box.KeyPair ephemeralKeyPair = getEphemeralKeyPair();
byte[] nonce = getNonce();
TweetNaclFast.Box box = new TweetNaclFast.Box(ephemeralKeyPair.getPublicKey(), baseKeyPair.getSecretKey());
byte[] boxToOpen = Base64.getDecoder().decode(encryptedData);
byte[] decryptedData = box.open(boxToOpen, nonce);
return new String(decryptedData, StandardCharsets.UTF_8);
}
> Please, note these two lines
> TweetNaclFast.Box box = new TweetNaclFast.Box(baseKeyPair.getPublicKey(), ephemeralKeyPair.getSecretKey());
> TweetNaclFast.Box box = new TweetNaclFast.Box(ephemeralKeyPair.getPublicKey(), baseKeyPair.getSecretKey());
return encryptAndDecryptData.encrypt("Friday"); // JHo/tk/Jpp2rpxpzIIgBhVhK/CBZLg==
return encryptAndDecryptData.decrypt("JHo/tk/Jpp2rpxpzIIgBhVhK/CBZLg==") //Friday
I am trying to write Javascript to match the output from this Java code:
Java:
import java.util.Base64;
public class Enc2 {
public static void main (String[] arg) {
System.out.println(encryptSomeNumber("1234567812345678"));
}
public static String encryptSomeNumber(final String SomeNumber){
String encryptedSomeNum = "";
String ALGO = "AES";
try {
String myKey = "DLDiGPqGysAow3II";
byte[] keyBytes = myKey.getBytes("UTF-8");
java.security.Key encryptkey = new javax.crypto.spec.SecretKeySpec(keyBytes, ALGO);
javax.crypto.Cipher c;
c = javax.crypto.Cipher.getInstance(ALGO);
c.init(javax.crypto.Cipher.ENCRYPT_MODE, encryptkey);
byte[] encVal = c.doFinal(SomeNumber.getBytes());
byte[] encodedBytes = Base64.getEncoder().encode(encVal);
String s = new String(encodedBytes);
encryptedSomeNum = s;
} catch (Exception e) {
System.out.println("error when encrypting number");
return encryptedSomeNum;
}
return encryptedSomeNum;
}
}
Output: Wrs66TuAIxYe+M4fqyyxtkyMFkWGwx9i45+oQfEA4Xs=
Javascript that I have so far (nodeJS v8.7.0):
let crypto = require('crypto');
let algorithm = 'aes-128-ecb';
let password = 'DLDiGPqGysAow3II';
function encrypt(buffer){
let cipher = crypto.createCipher(algorithm, password)
let crypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
return crypted;
}
let cyphertext = encrypt(new Buffer("1234567812345678", "utf8"))
console.log(cyphertext.toString('base64'));
Output: m1jnKjBbKu+m/zsf9DBTMo3NL4E035l0EailFjt/qjo=
Can anyone see what I'm missing here? Something with PKCS padding?
No, the padding is the same. The problem is that there are two createCipher methods. One is using a password and a key derivation function over the password - this is the one you are using now. The other one uses key and IV. Of course, ECB doesn't use an IV, so you may have to supply an IV value that is then not used.