node.jsencryptiondesnode-crypto

NodeJS "createDecipheriv" method isn't working with encrypted text in Java


I'm trying to decrypt a legacy database with NodeJS, but without success. The code was initially written in Java.

EDITED

The Java source code to encrypt data.

import java.util.*;

import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.SecretKey;
import javax.crypto.spec.DESKeySpec;
import java.util.Base64;

public class Main {
    public static void main(String[] args) {

        String encrypted = "";
      
        String secretCredentials = "abcdefghijklmnop";

        String plainText = "Text to encrypt!";
      
        String charset = StandardCharsets.UTF_8;

        String strategy = "DES";
      
        try {
        
            byte[] secretCredentialsBytes = secretCredentials.getBytes(charset);
        
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(strategy);
        
            SecretKey secretKey = secretKeyFactory.generateSecret(new DESKeySpec(secretCredentialsBytes));
        
            Cipher cipher = Cipher.getInstance(strategy);
            
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            
            byte[] plainTextBytes = plainText.getBytes(charset);
              
            byte[] outputBytes = cipher.doFinal(plainTextBytes);
        
            byte[] encryptedTextBytes = Base64.getEncoder().encode(outputBytes);
  
            encryptedText = new String(encryptedTextBytes);
            
            System.out.println(encryptedText); //=> AIGBXYEWXz5w2Z3Fjqe5YiQbyR5eVbPW
          
        } catch (Exception e) {
            e.printStackTrace();
        }
      
    }
}

The NodeJS source code to decrypt data:

const {
  createDecipheriv
} = await import('crypto');

const encrypted = 'AIGBXYEWXz5w2Z3Fjqe5YiQbyR5eVbPW';

const charset = 'utf-8';

const encryptedAsBuffer = Buffer.from(encrypted, 'base64');

const secretCredentials = 'abcdefghijklmnop';

let credentialsAsBuffer  = Buffer.from(secretCredentials, charset);

const strategy = 'des-ede-cbc';

const iv = credentialsAsBuffer.subarray(credentialsAsBuffer.length - (credentialsAsBuffer.length - 8));

const decipher = createDecipheriv(strategy, credentialsAsBuffer, iv);

let decrypted = decipher.update(encryptedAsBuffer, 'base64', charset);
decrypted += decipher.final(charset);

console.log(decrypted);

But an error is returning...

node:internal/crypto/cipher:193
  const ret = this[kHandle].final();
                            ^

Error: error:1C800064:Provider routines::bad decrypt
    at Decipheriv.final (node:internal/crypto/cipher:193:29)
    at file:///... {
  library: 'Provider routines',
  reason: 'bad decrypt',
  code: 'ERR_OSSL_BAD_DECRYPT'
}

Node.js v20.11.0

I know that was encountered vulnerabilities on "DES" algorithm, but it's a legacy system.


Solution

  • Please note: In the meantime, you have changed your algorithm from DES to Triple DES, but have not adjusted the key, so that the actual purpose of imitating DES with Triple DES fails.
    In the following, I refer to your original post where des (equivalent to des-cbc) was specified as algorithm (called strategy in your NodeJS code).


    The Java code uses provider- and platform-dependent settings or makes adjustments under the hood, which must first be identified so that they can be correctly ported to NodeJS:


    Another problem is that your NodeJS environment does not support the deprecated DES. However, according to your test Triple DES is supported.
    TripleDES is a triple consecutive execution of DES (as encryption/decryption/encryption) to increase security (but with a corresponding decrease in performance), with at least two different DES keys (2TDEA) or, more securely, with three different DES keys (3TDEA), s. Keying options for more details.
    If the same DES key is used instead of different DES keys, Triple DES is reduced to DES. In this way, DES can be mimicked with Triple DES. For the 2TDEA variant, the 8 bytes DES key must be concatenated with itself to a 16 bytes key (DES key | DES key).


    Taking all these points into account, a possible NodeJS port of the Java code is:

    const encrypted = 'AIGBXYEWXz5w2Z3Fjqe5YiQbyR5eVbPW';
    
    const stringKey = 'abcdefghijklmnop';
    let keyAsBuffer = Buffer.from(stringKey, 'utf-8').subarray(0,8); // cut the key
    keyAsBuffer = Buffer.concat([keyAsBuffer, keyAsBuffer]); // DES key | DES key
    
    const strategy = 'des-ede-ecb'; // Triple DES in 2TDEA variant, ECB mode 
    const decipher = crypto.createDecipheriv(strategy, keyAsBuffer, null);
    let decrypted = decipher.update(encrypted, 'base64', 'utf-8');
    decrypted += decipher.final('utf-8');
    
    console.log(decrypted); // Text to encrypt!
    

    Note that des-ede may also be used when specifying the algorithm, as this is an alias for des-ede-ecb, although the explicit specification of the mode is more transparent (caution: for DES, des is an alias for des-cbc). For an overview of the specifiers, see e.g. openssl-enc, sec. Supported Ciphers.
    For the sake of completeness: Triple DES in the 3TDEA variant is specified (for ECB) with des-ede3-ecb (or des-ede3). In this case, the key would have to be concatenated to a 24 bytes key (DES key | DES key | DES key).


    Security:
    As you have already mentioned yourself: DES is outdated (deprecated about 20 years ago) and insecure. Triple DES in the 3TDEA variant is more secure (now also deprecated), but comparatively inperformant. The current standard for symmetric encryption is AES.
    ECB is also insecure. A mode with an IV (e.g. CBC) is more secure, authenticated encryption such as GCM even more so.
    Another vulnerability is the direct derivation of a key from a string (due to its generally lower entropy) using only a charset encoding. If a passphrase is applied, the key should be derived with a key derivation function such as Argon2 or at least PBKDF2 in conjunction with a random salt.