Search code examples
cencryptionopensslcryptographyevp-cipher

C Encryption logic not matching with java


I am looking for equivalent C code for below java code. i am trying to write two application one in java and other in C. Java application encrypt/decrypt "string" with below logic, and it is working when using below java method.

public class AEScrypt {

    public static String encryptString(String strToEncrypt, String secret, String salt, byte[] iv) {
        try {

            IvParameterSpec ivspec = new IvParameterSpec(iv);

            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            KeySpec keySpec = new PBEKeySpec(secret.toCharArray(), salt.getBytes(), 65536, 256);
            SecretKey secretKeySpec = keyFactory.generateSecret(keySpec);
            SecretKeySpec secretKey = new SecretKeySpec(secretKeySpec.getEncoded(), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivspec);
            int length = 0;
            if (strToEncrypt.length() <= 16) {
                length = 16;
            } else if (strToEncrypt.length() > 16 && strToEncrypt.length() <= 32) {
                length = 16;
            }
            strToEncrypt = fixedLengthString(strToEncrypt, length);
            return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception exception) {
            System.out.println("Error while encrypting value : "+exception.getMessage());
        }
        return null;
    }

    private static String fixedLengthString(String string, int length) {
        return String.format("%" + length + "s", string);
    }

    public static String decryptString(String strToDecrypt, String secret, String salt, byte[] iv) {
        try {
            IvParameterSpec ivspec = new IvParameterSpec(iv);

            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            KeySpec keySpec = new PBEKeySpec(secret.toCharArray(), salt.getBytes(), 65536, 256);
            SecretKey secretKeySpec = keyFactory.generateSecret(keySpec);
            SecretKeySpec secretKey = new SecretKeySpec(secretKeySpec.getEncoded(), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, secretKey, ivspec);
            return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt))).trim();
        } catch (Exception e) {
            e.getMessage();
        }
        return null;
    }
}

What i understand from above JAVA code are:

FOR ENCRYPTION:

  1. We are using HMAC-sha256 for generating "key", which takes "salt", "password".
  2. Padding input data.
  3. We are using AES-CBC-256 for Encrypting padded input data, using above generated "key" and "iv".
  4. We encoding with base64 the encryption data.

FOR DECRYPTION:

  1. We are using HMAC-sha256 for generating "key", which takes "salt", "password".
  2. We decoding with base64 and getting encrypted data.
  3. We are using AES-CBC-256 for Decrypting encrypted data, using above generated key and iv.
  4. Trim the decrypted data.

For replicating same in C, I used encryption/Decryption method from below link; EVP symmetric Encryption And Decryption

For generating the key i used "PKCS5_PBKDF2_HMAC" and EVP_MD as "EVP_sha256()".

int PKCS5_PBKDF2_HMAC(const char *pass, int passlen,
                       const unsigned char *salt, int saltlen, int iter,
                       const EVP_MD *digest,
                       int keylen, unsigned char *out);

For base64 encoding/decoding: base64 Encoding/decoding

Also i have taken care of padding and trim logic. But I am getting different encrypted data from java and c code. Am i missing something here?

If you have any sample function in C that will be very helpful.


Solution

  • FOR ENCRYPTION:

    1. We are using HMAC-sha256 for generating "key", which takes "salt", "password".

    No, you are using PBKDF2 WITH HMAC-SHA256. That's not at all the same thing as plain HMAC-SHA256. However, the OpenSSL function you identified DOES match this assuming you give it the correct parameters. This also applies to decryption step 1.

    1. Padding input data.

    Sort of. That padding only works correctly for input data that is up to 16 characters, all of which are ASCII (because you encode it as UTF-8, and any non-ASCII character produces more than one byte, making the encoded value an illegal length). Most longer values will fail, although a few will succeed by bad luck. And even for the values that 'succeed', some will be changed; this is considered bad practice and essentially all competently designed crypto schemes since about 1980 are designed to preserve all data. In particular the very common PKCS5 (sometimes called PKCS7 or PKCS5/PKCS7 for technical reasons) standard padding preserves all data correctly and is already implemented in both Java and OpenSSL, as well as nearly all other decent crypto libraries and devices, and would be a better choice as well as simpler.

    With the padding fixed, the Java side could do non-ASCII data, but only if you both encode the plaintext to be encrypted and decode the plaintext after decryption suitably. You have the .getBytes(StandardCharsets.UTF_8) on encrypt, but need to match it with new String(cipher.doFinal(...), StandardCharsets.UTF_8) on decrypt, otherwise it may or may not work depending on the platform and environment you use to run it.

    The C side may be harder. OpenSSL is based on old-school C code started before the 1995 and 1999 versions of C started to handle non-English characters, and it understands only bytes, which can be single-byte aka 'narrow' characters. Either you must wrap it with calling code that handles 'wide' characters in a multi-byte encoding such as UTF-8 (and calls the OpenSSL parts using bytes), or you must do this outside the program, by controlling the environment (such as the terminal or emulator) or files. Your question doesn't provide even a hint about any of those, so it's impossible to make any recommendation(s).

    Because you treat the 'secret' (password), salt, and IV as Strings, the same considerations apply to them, except that they are likely to come from different source(s) than the data. IV and salt are designed to be byte sequences, and restricting IV in particular to ASCII or even UTF-8 encodings probably reduces security some, but as the topic of SO is programming and not security I won't pursue that. In actual PBKDF2 in PKCS5 password is also octets (Java bytes), but it 'recommends' that text (characters) be encoded as ASCII or UTF-8, and Java does take char[] in PBEKeySpec and encode as UTF-8, so for non-ASCII the OpenSSL caller or environment would need to match that.

    Given those limitations: all values are ASCII only, data is not more than 16 chars=bytes and IV is exactly 16, the following C code matches and could interoperate with your Java. Error handling is minimal, and I do both encrypt and decrypt in a single function; you would want to be able to separate them. (corrected)

    /* SO65195128.c 20dec09,11 */
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <openssl/evp.h>
    #include <openssl/err.h>
    #include <openssl/ssl.h>
    
    void err (int n, const char * s){ printf("%s:%d\n", s, n); ERR_print_errors_fp(stdout); exit(1); }
    
    int main (int argc, char **argv){
        if( argc != 5 || strlen(argv[3]) != 16 || strlen(argv[4]) > 16 ){ printf("bad args\n"); exit(1); }
        const char * pw = argv[1], * salt = argv[2], * iv = argv[3], * org = argv[4];
        unsigned char key [32], pad [16], enc [16], b64 [25], unb [16], dec [16];
        int rc, len, temp, i, j;
        SSL_library_init();
    
        /* for both */
        rc = PKCS5_PBKDF2_HMAC (pw, strlen(pw), (unsigned char*)salt, strlen(salt), 65536, EVP_sha256(), 32, key);
        if( rc != 1 ) err(rc,"PBKDF2");
        EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
    
        /* for encrypt */
        len = strlen(org); memset(pad, ' ', 16-len); memcpy (pad+16-len, org, len);
        rc = EVP_EncryptInit (ctx, EVP_aes_256_cbc(), key, (unsigned char*)iv);
        if( rc != 1 ) err(rc,"EncryptInit");
        rc = EVP_CIPHER_CTX_set_padding(ctx,0);
        if( rc != 1 ) err(rc,"set_padding");
        rc = EVP_EncryptUpdate (ctx, enc, &len, pad, 16);
        if( rc != 1 || len != 16 ) err(rc,"EncryptUpdate");
        rc = EVP_EncryptFinal (ctx, enc+len, &temp); 
        if( rc != 1 || temp != 0 ) err(rc,"EncryptFinal");
        rc = EVP_EncodeBlock(b64, enc, 16);
        if( rc <= 0 ) err(rc,"EncodeBlock");
    
        printf ("%.*s\n", rc, b64);
    
        /* for decrypt */
        rc = EVP_DecodeBlock(unb, b64, /*len*/rc)-(b64[rc-1]=='=')-(b64[rc-2]=='=');
        /* this is a hack, should go for DecodeInit,Update,Final */
        if( rc != 16 ) err(rc,"DecodeBlock");
        rc = EVP_DecryptInit (ctx, EVP_aes_256_cbc(), key, (unsigned char*)iv);
        if( rc != 1 ) err(rc,"DecryptInit");
        rc = EVP_CIPHER_CTX_set_padding(ctx,0);
        if( rc != 1 ) err(rc,"set_padding");
        rc = EVP_DecryptUpdate (ctx, dec, &len, unb, 16);
        if( rc != 1 || len != 16 ) err(rc,"DecryptUpdate");
        rc = EVP_DecryptFinal (ctx, dec+len, &temp); 
        if( rc != 1 || temp != 0 ) err(rc,"DecryptFinal");
        i=0; while(i<16&&dec[i]<=' ') i++; j=16; while(j>0&&dec[j-1]<=' ') j--;
    
        printf ("%.*s\n", j-i, dec+i);
        /* note this is NOT a C string -- needs to be copied and NUL added for that */
    
        return 0;
    }
    

    (added) with password/secret SEKRIT salt NOSAILOR (a joke) and IV STARTINGSTARTING
    for data SOMEDATA I get MOOn6FicaVcnVLokSANQsw==
    for data BUTNOTMUCH I get 5NsJUO4z1Bbap0U85ZClMg==
    both of which match what I get from your Java, and decrypt correctly. See what you get.