Search code examples
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:

    • The Java code only specifies the algorithm, not the mode and not the padding. In this case, provider-dependent default values are used.
      For the SunJCE provider, this means that ECB is applied as mode and PKCS#7 as padding. ECB in turn means that no IV is involved at all. Note that it is more transparent to specify mode and padding, e.g:

      ...
      Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
      ...
      

      The current NodeJS code uses the CBC mode, which must therefore be changed to ECB for compatibility with the Java code.

    • The key material you use (abcdefghijklmnop) is 128 bits (16 bytes) in size. DES only applies a 64 bits key (of which only 56 bits are used, the remaining 8 bits are parity bits). The Java implementation only uses the first 8 bytes of the key material, the rest is ignored. The Java code is therefore equivalent to:

      ...
      secretCredentialsBytes = Arrays.copyOf(secretCredentialsBytes, 8);
      ...
      

      In contrast to the Java code, the 16 bytes key is not shortened implicitly in the NodeJS code. This must be done explicitly, otherwise a RangeError: Invalid key length is thrown.

    • In addition, the encoding should be specified in the Java code for String/byte[] conversions, otherwise the platform-dependent default encoding will be used. With regard to your test data, this is most likely UTF-8 for your environment. Equivalent, but more transparent:

      ...
      byte[] plainTextBytes = plainText.getBytes(StandardCharsets.UTF_8);
      ...
      String encryptedText = new String(encryptedTextBytes, 
      StandardCharsets.UTF_8);
      ...
      

      This does not require an adjustment on the NodeJS side, as UTF-8 is already used there.


    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.