C 加密逻辑与 java 不匹配

C Encryption logic not matching with java

我正在为以下 java 代码寻找等效的 C 代码。 我正在尝试编写两个应用程序,一个在 java 中,另一个在 C 中。 Java application encrypt/decrypt "string" 具有以下逻辑,并且在使用以下 java 方法时有效。

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;
    }
}

我从上面的JAVA代码中了解到的是:

加密:

  1. 我们正在使用 HMAC-sha256 生成“密钥”,它采用“盐”, “密码”。
  2. 填充输入数据。
  3. 我们正在使用 AES-CBC-256 加密填充的输入数据,使用 上面生成的“key”和“iv”。
  4. 我们对加密数据进行base64编码。

解密:

  1. 我们正在使用 HMAC-sha256 生成“密钥”,它采用“盐”, “密码”。
  2. 我们用base64解码得到加密数据。
  3. 我们正在使用 AES-CBC-256 解密加密数据,使用上面 生成的密钥和 iv.
  4. Trim解密后的数据。

为了在 C 中复制相同内容,我使用了下面 link 中的 encryption/Decryption 方法; EVP symmetric Encryption And Decryption

为了生成密钥,我将“PKCS5_PBKDF2_HMAC”和EVP_MD用作“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);

对于 base64 encoding/decoding: base64 Encoding/decoding

我还处理了填充和 trim 逻辑。但是我从 java 和 c 代码中得到了不同的加密数据。我在这里遗漏了什么吗?

如果您有任何 C 语言示例函数,那将非常有帮助。

FOR ENCRYPTION:

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

不,您正在使用 PBKDF2 WITH HMAC-SHA256。这与普通的 HMAC-SHA256 完全不同。但是,假设您为其提供了正确的参数,您确定的 OpenSSL 函数确实与此匹配。这也适用于解密步骤 1。

  1. Padding input data.

有点。该填充仅适用于最多 16 个字符的输入数据,所有字符都是 ASCII(因为您将其编码为 UTF-8,并且任何非 ASCII 字符都会产生一个以上的字节,从而使编码值成为非法长度) .大多数较长的值都会失败,尽管有一些会因运气不佳而成功。甚至对于 'succeed' 的值,也会更改一些;这被认为是不好的做法,基本上自 1980 年以来所有精心设计的加密方案都旨在保留 所有 数据。特别是非常常见的 PKCS5(有时由于技术原因称为 PKCS7 或 PKCS5/PKCS7)标准填充可以正确保留所有数据,并且已经在 Java 和 OpenSSL 以及几乎所有其他体面的加密库和设备,将是一个更好的选择,也更简单。

在固定填充的情况下,Java 端可以处理非 ASCII 数据,但前提是您同时对要加密的明文进行编码并在解密后对明文进行解码 适当。您在加密时有 .getBytes(StandardCharsets.UTF_8),但需要在解密时将其与 new String(cipher.doFinal(...), StandardCharsets.UTF_8) 匹配,否则它可能会或可能不会工作,具体取决于您用来 运行 它的平台和环境。

C面可能更难。 OpenSSL 基于在 1995 和 1999 版本的 C 开始处理非英语字符之前开始的老派 C 代码,它只理解字节,可以是单字节又名 'narrow' 字符。您必须使用处理 'wide' 多字节编码(例如 UTF-8)中的字符的调用代码将其包装起来(并使用字节调用 OpenSSL 部分),或者您必须在程序外部通过控制环境来执行此操作(例如终端或模拟器)或文件。你的问题甚至没有提供任何关于这些的提示,所以不可能提出任何建议。

因为您将 'secret'(密码)、salt 和 IV 视为 Strings,所以同样的注意事项适用于它们,除了它们可能来自不同的来源比数据。 IV 和 salt 设计 为字节序列,并且将 IV 特别限制为 ASCII 甚至 UTF-8 编码可能会降低一些安全性,但由于 SO 的主题是编程而不是安全性我不会追求那个。在实际的 PBKDF2 中,PKCS5 密码也是八位字节(Java 字节),但是 'recommends' 文本(字符)被编码为 ASCII 或 UTF-8,并且 Java 需要 char[]PBEKeySpec 中编码为 UTF-8,因此对于非 ASCII,OpenSSL 调用者或环境需要匹配它。

考虑到这些限制:所有值仅为 ASCII,数据不超过 16 个字符=字节,IV 恰好为 16,以下 C 代码匹配并且可以与您的 Java 互操作。错误处理最少,我在一个函数中同时进行加密和解密;你会希望能够将它们分开。 (更正)

/* 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;
}

(添加)与 password/secret SEKRIT salt NOSAILOR(一个笑话)和 IV STARTINGSTARTING
对于数据 SOMEDATA,我得到 MOOn6FicaVcnVLokSANQsw==
对于数据 BUTNOTMUCH 我得到 5NsJUO4z1Bbap0U85ZClMg==
两者都与我从您的 Java 那里得到的相匹配,并且正确解密。看看你得到了什么。