1

I have C# code which decrypts encrypted token passed by another application. I can not change this part. Now i'm writing an application in java which will encrypt my token, that would be passed to C# application.

I'm not able to match encrypted string with java code. Any help would be appreciated. Thank you.

C# Code

public class Crypto
{
    private TripleDES DesInstance = null;

    public Crypto(string key)
    {
        byte[] password = Encoding.GetEncoding(1252).GetBytes(key);
        DesInstance = new System.Security.Cryptography.TripleDESCryptoServiceProvider();
        PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, null);
        DesInstance.IV = new byte[8];
        DesInstance.Key = pdb.CryptDeriveKey("TripleDES", "SHA1", 192, DesInstance.IV);
    }

    public string Decrypt(string cipheredText)
    {
        byte[] cipherText = StringToByteArray(cipheredText);
        string plainText = null;

        ICryptoTransform transform = DesInstance.CreateDecryptor();

        MemoryStream memStreamEncryptedData = new MemoryStream(cipherText, 0, cipherText.Length - 1);
        CryptoStream encStream = new CryptoStream(memStreamEncryptedData, transform, CryptoStreamMode.Read);
        using (StreamReader srDecrypt = new StreamReader(encStream, Encoding.GetEncoding(1252)))
        {
            plainText = srDecrypt.ReadToEnd();
        }

        return plainText;
    }

    private byte[] StringToByteArray(string hex)
    {
        return Enumerable.Range(0, hex.Length)
                         .Where(x => x % 2 == 0)
                         .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                         .ToArray();
    }
}

Java Code

public class TripleDes {

    private static final String UNICODE_FORMAT = "UTF-8";
    public static final String DESEDE_ENCRYPTION_SCHEME = "DESede";
    private KeySpec ks;
    private SecretKeyFactory skf;
    private Cipher cipher;
    byte[] arrayBytes;
    private String myEncryptionKey;
    private String myEncryptionScheme;
    SecretKey key;

    public TripleDes() throws Exception {
        myEncryptionKey = "045e466ccc34a1f1688640d0441601b7ae2c";
        myEncryptionScheme = DESEDE_ENCRYPTION_SCHEME;
        arrayBytes = myEncryptionKey.getBytes(UNICODE_FORMAT);
        ks = new DESedeKeySpec(arrayBytes);
        skf = SecretKeyFactory.getInstance(myEncryptionScheme);
        cipher = Cipher.getInstance(myEncryptionScheme);
        key = skf.generateSecret(ks);
    }

    public String encrypt(String unencryptedString) {
        String encryptedString = null;
        try {
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] plainText = unencryptedString.getBytes(UNICODE_FORMAT);
            byte[] encryptedText = cipher.doFinal(plainText);
            encryptedString = new String(Base64.encodeBase64(encryptedText));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return encryptedString;
    }

    public String decrypt(String encryptedString) {
        String decryptedText = null;
        try {
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] encryptedText = Base64.decodeBase64(encryptedString);
            byte[] plainText = cipher.doFinal(encryptedText);
            decryptedText = new String(plainText);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return decryptedText;
    }
}
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Is `myEncryptionKey` the same as the string being passed to the `Crypto` constructor? – Jim Rhodes Feb 26 '22 at 21:43
  • @JimRhodes Does it matter if no attempt is even made to implement `PasswordDeriveBytes`? Note that that is basically PBKDF1, but with "special" Microsoft code to extend it if the result is larger than the hash, in this case SHA-1. Special in the sense that it is entirely insecure and **should** be replaced directly. – Maarten Bodewes Feb 26 '22 at 22:09
  • The C# code does not use the algorithm based on PBKDF1 extended by MS, but the algorithm from MS CAPI's `CryptoDeriveKey()`. – Topaco Feb 28 '22 at 08:46

1 Answers1

1

The C# code uses PasswordDeriveBytes#CryptDeriveKey() as key derivation function which is a wrapper around CAPI's CryptDeriveKey() function (see here). This functionality is missing in the Java code and is one of the reasons for the incompatibility.

PasswordDeriveBytes#CryptDeriveKey() is not to be confused with PasswordDeriveBytes#GetBytes(). The latter uses the (by MS) extended implementation of the PBKDF1 algorithm mentioned in the comments, which is not applied here.

The algorithm of CryptDeriveKey() is described here and an implementation can be found here:

public static byte[] CryptDeriveKey(byte[] hBaseData, String hashAlgorithm, int requiredLength) throws NoSuchAlgorithmException {
    int keyLength = hBaseData.length;
    byte[] derivedKey = new byte[requiredLength];
    if (keyLength >= requiredLength) {
        for (int i = 0; i < requiredLength; i++) {
            derivedKey[i] = hBaseData[i];
        }
        return derivedKey;
    }

    byte[] buff1 = new byte[64];
    byte[] buff2 = new byte[64];
    Arrays.fill(buff1, (byte) 0x36);
    Arrays.fill(buff2, (byte) 0x5C);
    for (int i = 0; i < keyLength; i++) {
        buff1[i] ^= hBaseData[i];
        buff2[i] ^= hBaseData[i];
    }

    MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
    md.reset();
    byte[] result1 = md.digest(buff1);
    md.reset();
    byte[] result2 = md.digest(buff2);

    for (int i = 0; i < requiredLength; i++) {
        if (i < result1.length)
            derivedKey[i] = result1[i];
        else
            derivedKey[i] = result2[i - result1.length];
    }
    return derivedKey;
}

Note that CryptDeriveKey() does not expect the passphrase as first parameter, but the hash of the passphrase.

Example of usage:

String passphrase = "my passphrase";
String digestName = "SHA-1"; 
MessageDigest md = MessageDigest.getInstance(digestName);
byte[] hash = md.digest(passphrase.getBytes("Cp1252"));
byte[] key = CryptDeriveKey(hash, digestName, 24);
System.out.println(HexFormat.of().formatHex(key)); // 5de508082bfe0924356d881c8cde15a540c92f267ef5a416

Note that in the case of DES or TripleDES the parity bits are not adjusted. This happens implicitly in SecretKeyFactory#generateSecret().


Additional issues: Apart from key derivation, the CBC mode with a zero IV, PKCS#7 padding and the appropriate encodings for plaintext (Cp1252) and ciphertext (hex encoding) are to be used in the Java code. Since the C# code removes the last byte of the ciphertext for unknown reasons, a dummy byte must be added to the end of the ciphertext in the Java code.

Although the C# code can not be changed according to the description, the following notes on vulnerabilities: The key derivation is weak (this applies to both CryptDeriveKey() and PBKDF1) and should be replaced by a dedicated password-based key derivation like Argon2 or PBKDF2. A static IV (like a zero IV) is also weak. TripleDES is deprecated and slow and should be replaced with a modern algorithm like AES.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Thanks @Topaco. I am able to encrypt in java and decrypt in C# and data matches. However, if my input string contains extended ASCII characters (for ex. Latin small letter y with acute- ý), that's returning some incorrect character in place of that. What do you suggest, why it happens that way. – Mahendra Singh Feb 28 '22 at 16:28
  • @MahendraSingh, your C# code is using `Encoding.GetEncoding(1252).GetBytes(key)` but your Java code uses `myEncryptionKey.getBytes(UTF-8)`. – Jim Rhodes Feb 28 '22 at 16:44
  • @MahendraSingh - This is probably due to the different encodings of the plaintext, which I addressed in the second section. The C# code uses Cp1252, while the Java code uses UTF-8. You must also use Cp1252 in the Java code if the C# code is not allowed to be changed. – Topaco Feb 28 '22 at 16:48