2

I want to rewrite old C code from 1990 to Go. But the pain point here is migration of cryptographic algorithms. I have the following Cpp code that successfully decrypts the cipher into plain text.

#include <windows.h>
#include <wincrypt.h>
#include <string>
#include <iostream>

using namespace std;

int main() {
    auto name = L"aaaa";

    HCRYPTPROV hProv;

    if (!CryptAcquireContext(&hProv, name, MS_DEF_PROV, PROV_RSA_FULL, 0) &&
        !CryptAcquireContextW(&hProv, name, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_NEWKEYSET) &&
        !CryptAcquireContextW(&hProv, name, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_MACHINE_KEYSET) &&
        !CryptAcquireContextW(&hProv, name, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_MACHINE_KEYSET | CRYPT_NEWKEYSET)) {
        cout << "fail" << endl;
        exit(1);
    }

    HCRYPTHASH hHash;

    if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) {
        cout << "fail" << endl;
        exit(1);
    }

    const BYTE* pwd = reinterpret_cast<const BYTE*>("-+ REDACTED +-");

    if (!CryptHashData(hHash, pwd, 14, 0)) {
        cout << "md5 failure" << endl;
        exit(1);
    }

    HCRYPTKEY hKey;

    if (!CryptDeriveKey(hProv, CALG_RC2, hHash, 0, &hKey)) {
        cout << "failure" << endl;
        exit(1);
    }

    unsigned char cipher[] = {52, 54, 253, 199, 131, 110, 202, 15, 185, 107, 71, 244, 150, 171, 220, 6, 183, 86, 234, 252, 242, 84, 156, 200};

    DWORD len = 24;

    if (!CryptDecrypt(hKey, 0, TRUE, 0, cipher, &len)) {
        printf("%x\n", GetLastError());
        exit(1);
    }

    for (int i = 0; i < len; i++) {
        printf("%d, ", cipher[i]);
    }
}

and I have the following Go code:

package main

import (
    "crypto/md5"
    "fmt"

    "github.com/dgryski/go-rc2"
)

func main() {
    pwd := "-+ REDACTED +-"
    hash := md5.Sum([]byte(pwd))

    alg, err := rc2.New(hash[:], 128)
    if err != nil {
        panic(err)
    }

    cipher := []byte{52, 54, 253, 199, 131, 110, 202, 15, 185, 107, 71, 244, 150, 171, 220, 6, 183, 86, 234, 252, 242, 84, 156, 200}

    result := []byte{}
    dst := make([]byte, 8)

    for i := 8; i <= len(cipher); i += 8 {
        alg.Decrypt(dst, cipher[i-8:i])
        result = append(result, dst...)
    }

    fmt.Println(result)
}

run online: https://go.dev/play/p/veRMRShmtnw

The output of them are different. While the CPP version able to decrypt correctly.

My assumption is, that `CryptDeriveKey` harden the initial hash password with additional values and as a result encryption key gets altered.

maksadbek
  • 1,508
  • 2
  • 15
  • 28
  • CryptDeriveKey() does "some stuff" that you'll need to replicate in your Go code. I don't know the specifics but I'm sure you can Google it. – Luke Aug 09 '23 at 21:56
  • I see two issues: 1 the value 14 in the original code probably cuts off the password after 14 bytes and 2: calling the cipher multiple times *could* create a different result than performing one call. – Maarten Bodewes Aug 09 '23 at 22:23
  • @Luke I don't see that. I've asked ChatGPT and it mainly seems to take the hash result and converts it into a key structure. I don't think it changes the value at all. – Maarten Bodewes Aug 09 '23 at 22:45
  • 1
    Use your brain instead of ChatGPT: https://crypto.stackexchange.com/a/32604 – Luke Aug 10 '23 at 06:54
  • @Luke Thanks! I've tried to use this library github.com/BitsOfBinary/go-cryptderivekey to replicate CryptDeriveKey (not sure if it calculates correct derived key) but outputs are not still the same. – maksadbek Aug 10 '23 at 08:39
  • Apparently the algorithm I linked is only used for certain cases which your code doesn't meet. So yes, the key is just the hash of the password. The C++ and Go code produce the exact same output for me. – Luke Aug 10 '23 at 10:26
  • Ah, the issue is with the provider. You're using `MS_DEF_PROV`, the base provider, which [only supports 40-bit RC2 keys](https://learn.microsoft.com/en-us/windows/win32/seccrypto/base-provider-algorithms). Use `MS_ENHANCED_PROV`, the enhanced provider, which [supports 128-bit RC2 keys](https://learn.microsoft.com/en-us/windows/win32/seccrypto/enhanced-provider-algorithms). – Luke Aug 10 '23 at 10:30
  • @Luke could I use just 40 effective key length in my Go code instead ? UPD: I've tried it, but still difference output. idk maybe, what could be wrong that it works on your machine but not in mine. – maksadbek Aug 10 '23 at 10:40
  • The ciphertext you're using was encrypted with a 128-bit key. If you want to use 40-bit keys then you need to update the ciphertext with one encrypted by a 40-bit key. – Luke Aug 10 '23 at 12:42

1 Answers1

1

To understand the encryption/decryption in the C code, it is helpful to know the parameters used. With CryptGetKeyParam() the parameters can be output. This gives:

  • KP_KEYLEN, KP_EFFECTIVE_KEYLEN: Key length and effective key length are each 40 bits long.
  • KP_SALT: As salt a zero salt is used (11 times 0x00).
  • KP_MODE: As mode CBC is applied (value CRYPT_MODE_CBC).
  • KP_BLOCKLEN: The block length is 64 bits long.
  • KP_IV: As IV a zero IV is used (8 times 0x00, corresponding to the block length).
  • KP_PADDING: As padding PKCS#7 is applied (value: PKCS5_PADDING).

These settings must be applied when implementing the Go code.


The crucial point about the derived RC2 key is that it is generally composed of two parts. The first has the length KP_KEYLEN, the second is the salt, see here:

  • Regarding the first key part: It has already been mentioned in the comments that the MS Base Cryptographic Provider you are using (MS_DEF_PROV) supports a size of 40 bits for RC2 by default. This length refers to the first key part.

    To determine the algorithm that is applied to create the key, it is useful to determine the key first. The key can be exported with CryptExportKey() if the CRYPT_EXPORTABLE flag is set in CryptDeriveKey(). The format is BLOBHEADER (8 bytes)|DWORD key length (2 bytes)|key material (see here).
    The export proves that this key part (i.e. key material) corresponds to the first 40 bits of the MD5 hash of the input key (pwd), i.e. CryptDeriveKey() generates the hash of the specified digest from the input key and returns the preceding KP_KEYLEN bits of the hash as first key part.

  • The second key part is the salt. This can be controlled by various CryptDeriveKey() flags. If CRYPT_CREATE_SALT is set, a salt is applied, if this flag is not set, a salt consisting of 0x00 values is used (this corresponds to your case). If no salt is to be applied, CRYPT_NO_SALT must be set.

Example (assuming that the flags are set as in the C code):
If the input key is some test key, the MD5 hash of it is 0x9bf00ebbb4522bbf2a6209f00372b0a7. From this the first 5 bytes (40 bits) are used and the 11 bytes zero salt is appended, i.e. the final key is 16 bytes (128 bits) long and is 0x9bf00ebbb40000000000000000000000.


In addition to the specifics about the key, the following must be implemented in the Go code (according to the above parameters):

  • The effective key length is 40 bits (as opposed to the key length, which is 128 bits because of the salt).

  • As mode CBC is used, as IV a zero IV.

  • As padding PKCS#7 is applied.

With this information, a possible Go implementation is:

import (
    "crypto/cipher"
    "crypto/md5"
    "encoding/hex"
    "fmt"

    "github.com/dgryski/go-rc2"
    "github.com/zenazn/pkcs7pad"
)

func main() {
    ciphertext, _ := hex.DecodeString("596914c6020c9dbfc193e9e588a380730add8e1a69a4994ec57362dd1ad1d37a00932a64a385af6d8c5234b0c36d50c4")
    iv, _ := hex.DecodeString("0000000000000000")

    key := "some test key"
    hash := md5.Sum([]byte(key))

    finalKey := make([]byte, 16)
    copy(finalKey[:], hash[:5])
    fmt.Println(hex.EncodeToString(finalKey)) // 9bf00ebbb40000000000000000000000

    plaintextPadded := make([]byte, len(ciphertext))
    alg, _ := rc2.New(finalKey, 40)
    mode := cipher.NewCBCDecrypter(alg, iv)
    mode.CryptBlocks(plaintextPadded, ciphertext)
    plaintext, _ := pkcs7pad.Unpad(plaintextPadded)

    fmt.Println(hex.EncodeToString(plaintext)) // 54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67
    fmt.Println(string(plaintext))             // The quick brown fox jumps over the lazy dog
}

The test data can also be decrypted with the C code:

...
const char* pwd = "some test key";
CryptHashData(hHash, reinterpret_cast<const BYTE*>(pwd), strlen(pwd), 0);
...
unsigned char cipher[] = { 0x59, 0x69, 0x14, 0xc6, 0x02, 0x0c, 0x9d, 0xbf, 0xc1, 0x93, 0xe9, 0xe5, 0x88, 0xa3, 0x80, 0x73, 0x0a, 0xdd, 0x8e, 0x1a, 0x69, 0xa4, 0x99, 0x4e, 0xc5, 0x73, 0x62, 0xdd, 0x1a, 0xd1, 0xd3, 0x7a, 0x00, 0x93, 0x2a, 0x64, 0xa3, 0x85, 0xaf, 0x6d, 0x8c, 0x52, 0x34, 0xb0, 0xc3, 0x6d, 0x50, 0xc4 };
DWORD len = sizeof(cipher);
CryptDecrypt(hKey, 0, TRUE, 0, cipher, &len);
...

which shows that both codes are functionally identical.

Topaco
  • 40,594
  • 4
  • 35
  • 62