V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
youngs
V2EX  ›  程序员

PHP 的 AES 加密问题

  •  1
     
  •   youngs · 2020-04-26 17:55:04 +08:00 · 3105 次点击
    这是一个创建于 1663 天前的主题,其中的信息可能已经有所发展或是发生改变。

    为什么 PHP AES 解密 CBC 模式 iv 可以为空呢? 用 Java 去解密没有 iv 根本解密不出来啊,有什么办法解决?

    第 1 条附言  ·  2020-04-26 18:32:11 +08:00
    function h5AesDecode($tradePassword, $skey, $localIV='') {
            //$localIV = '0000000000000000';
            $encryptKey = $skey;
            $result = str_replace("-","+", $tradePassword);
            $tradePassword = str_replace("_","/", $result);
            //Open module
            $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, $localIV);
            mcrypt_generic_init($module, $encryptKey, $localIV);
            $encryptedData = base64_decode($tradePassword);
            $string = mdecrypt_generic($module, $encryptedData);
            $newString = '';
            for ($i =0; isset($string[$i]); $i++)  {
                $asc_code = ord($string[$i]); //得到其asc码
                //以下代码旨在过滤非法字符
                if ($asc_code == 9 || $asc_code == 10 || $asc_code == 13){
                    $newString .= ' ';
                } else if ($asc_code > 31 && $asc_code != 127) {
                    $newString .= $string[$i];
                }
            }
            return trim($newString);
        }
    
    第 2 条附言  ·  2020-04-27 15:51:24 +08:00

    密文来自前端CryptoJS加密,加密方法如下:

    function Encrypt_aes2(word, privateKey) {
    	var data = word;
            var key = CryptoJS.enc.Utf8.parse('1111111111111111');
            var iv = CryptoJS.enc.Utf8.parse('');
            //加密
            var encrypted = CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC });
            console.log('encrypted.toString()', encrypted.toString())
            // 这里是解密
            console.log('解密' ,CryptoJS.AES.decrypt(encrypted.toString(), key, { iv: iv, mode: CryptoJS.mode.CBC }).toString(CryptoJS.enc.Utf8))
            return encrypted.toString();
    }
    
    第 3 条附言  ·  2020-05-07 11:34:02 +08:00
    ## 无奈的最终办法:JAVA 调用 JS
    ### java 代码:
    ```
    public class AesJsUtil {
    public static String decrypt(String password, String key) {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
    Resource aesJs = new ClassPathResource("js" + File.separator + "aes.js");
    try {
    engine.eval(new FileReader(aesJs.getFile()));
    Invocable invocable = (Invocable) engine;
    return (String) invocable.invokeFunction("decrypt", password, key);
    } catch (Exception e) {
    log.error("JS 解密失败", e);
    }
    }
    }
    ```
    ### aes.js 代码:
    ```
    function decrypt(encrypt, currentKey) {
    var key = CryptoJS.enc.Utf8.parse(currentKey);
    var iv = CryptoJS.enc.Utf8.parse('');
    var decrypted = CryptoJS.AES.decrypt(encrypt.toString(), key, { iv: iv, mode: CryptoJS.mode.CBC }).toString(CryptoJS.enc.Utf8);
    return decrypted.toString();
    }
    ```
    第 4 条附言  ·  2020-05-07 11:34:21 +08:00

    无奈的最终办法:JAVA调用JS

    java代码:

    public class AesJsUtil {
        public static String decrypt(String password, String key) {
            ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
            Resource aesJs = new ClassPathResource("js" + File.separator + "aes.js");
            try {
                engine.eval(new FileReader(aesJs.getFile()));
                Invocable invocable = (Invocable) engine;
                return (String) invocable.invokeFunction("decrypt", password, key);
            } catch (Exception e) {
                log.error("JS解密失败", e);
            }
        }
    }
    

    aes.js代码:

    function decrypt(encrypt, currentKey) {
        var key = CryptoJS.enc.Utf8.parse(currentKey);
        var iv = CryptoJS.enc.Utf8.parse('');
        var decrypted = CryptoJS.AES.decrypt(encrypt.toString(), key, { iv: iv, mode: CryptoJS.mode.CBC }).toString(CryptoJS.enc.Utf8);
        return decrypted.toString();
    }
    
    19 条回复    2020-04-27 18:28:10 +08:00
    youngs
        1
    youngs  
    OP
       2020-04-26 17:59:56 +08:00
    同样的 js 的 CryptoJs 的 iv 也可以为空
    GTim
        2
    GTim  
       2020-04-26 18:09:43 +08:00
    那是因为人家 PHP 底层调用的是 `openssl` 家的函数,`openssl` 家的函数的默认值就是 16 个 0
    youngs
        3
    youngs  
    OP
       2020-04-26 18:18:12 +08:00
    @GTim 可是给 iv 赋 16 个 0,解密出来的不是原来的明文
    binkcn
        4
    binkcn  
       2020-04-26 18:27:41 +08:00
    @GTim

    似乎不是的,参见: https://blog.lancitou.net/how-to-generate-key-and-iv-in-openssl-aes/

    OPENSSL AES 算法中的 IV 是这样来的:

    ```
    hash1_128 = MD5(Passphrase + Salt)
    hash2_128 = MD5(hash1_128 + Passphrase + Salt)
    hash3_128 = MD5(hash2_128 + Passphrase + Salt)
    Key = hash1_128 + hash2_128
    IV = hash3_128
    ```

    另外,@youngs 不知道你用的是 PHP 什么版本,用的哪个函数在解 AES,最好有代码或者伪代码……
    youngs
        5
    youngs  
    OP
       2020-04-26 18:32:50 +08:00
    @binkcn
    代码已经加上了
    GTim
        6
    GTim  
       2020-04-26 19:25:14 +08:00
    @GTim CBC 模式是不需要 IV 的,你把你的 Java 代码也贴出来一下
    jim9606
        7
    jim9606  
       2020-04-26 20:34:00 +08:00
    CBC 、CFB 、OFB 等流密码模式都是需要 IV 的,不提供的就看默认值是啥了,反正必须要有。

    也就 ECB 不需要 IV (千万别用)
    rekulas
        8
    rekulas  
       2020-04-26 22:12:13 +08:00
    之前写过 c 版的 aes 虽然加了 iv 但是我不想记录这个 于是每次都是随机 iv 加密 空 iv 解密 解密后抛弃第一个块(因为第一个块无法解密) 后面的就是原始数据

    感觉你这问题和这有些关系 你可以使用相同的 iv(例如全 0 值) 打印解密后的原始数据看看一不一样
    xLuoBo
        9
    xLuoBo  
       2020-04-26 22:24:45 +08:00
    千万不要用 php 的 192 256 aes, key 和 iv 处理位数都和通用库不一样;
    他是一只特立独行的猪, 自己加的密只能自己解; 让别人懵逼去吧
    youngs
        11
    youngs  
    OP
       2020-04-27 10:12:40 +08:00
    @rekulas
    如果是全 0 的话和 Java 解密结果一致,但是都不是正确的解密结果。php 在 iv 为''的情况应该是有默认值,但是不知道这个默认值是啥
    youngs
        12
    youngs  
    OP
       2020-04-27 10:26:40 +08:00
    @rekulas 解密出的结果是 SVRQVSSUS***********************91856 确实第一个块解不出来,但是解密出来的密文相比较原来的密文也是查着第一个块的结果,第一个块解密出来是大写字母。。。
    binkcn
        13
    binkcn  
       2020-04-27 14:55:43 +08:00
    @youngs

    有个关键问题是:你的密文是用 PHP 加密的吗?

    我刚才测试了 PHP 下加密的时候 iv 参数不能为空,即 mcrypt_generic_init() 函数第 3 个参数不能为空,一定要设置的,而根据官网文档的说法是建议设置为全 0 值(长度根据 mcrypt_enc_get_iv_size 来设置)。

    所以现在问题的关键是你的密文是怎么加密的,openssl ?还是其他语言?
    youngs
        14
    youngs  
    OP
       2020-04-27 15:50:47 +08:00
    @binkcn 前端用 CryptoJS 加密的

    function Encrypt_aes2(word, privateKey) {
    var data = word;
    var key = CryptoJS.enc.Utf8.parse('1111111111111111');
    var iv = CryptoJS.enc.Utf8.parse('');
    //加密
    var encrypted = CryptoJS.AES.encrypt(data, key, { iv: iv, mode: CryptoJS.mode.CBC });
    console.log('encrypted.toString()', encrypted.toString())
    // 这里是解密
    console.log('解密' ,CryptoJS.AES.decrypt(encrypted.toString(), key, { iv: iv, mode: CryptoJS.mode.CBC }).toString(CryptoJS.enc.Utf8))
    return encrypted.toString();
    }
    binkcn
        15
    binkcn  
       2020-04-27 17:11:53 +08:00
    @youngs

    好了,话题终结了。

    看了下 CryptoJs 源码,和我在本帖 4 楼回复的猜想一样,如果你的 key 是个字符串,那么 CryptoJs 底层会用和 Openssl 兼容的函数来生成 iv:根据你的 key 和一个随机的 salt 经过若干次 md5 后生成的,每次 encrypt 的 iv 都不一样,具体方式见我 4 楼的伪代码。

    那么,怎么取到 CryptoJS 每次调用 AES.encrypt 时的 iv 呢?很简单……

    // Encrypt
    var ct = CryptoJS.AES.encrypt('my message', 'secret key 123');

    var saltHex = ct.salt.toString(); // random salt
    var ctHex = ct.ciphertext.toString(); // actual ciphertext
    var ivHex = ct.iv.toString(); // generated IV

    console.log(saltHex);
    console.log(ctHex);
    console.log(ivHex);


    你可以试试用打印出来的 iv 到 PHP 或者 Java 里面去解解看。
    binkcn
        16
    binkcn  
       2020-04-27 17:30:40 +08:00
    完整的加密、解密,以及显示本次加密的 iv,在我这里测试是通过的。祝你好运:)

    // Encrypt
    var ct = CryptoJS.AES.encrypt('my message', 'secret key 123');

    var saltHex = ct.salt.toString(); // random salt
    var ctHex = ct.ciphertext.toString(); // actual ciphertext
    var ivHex = ct.iv.toString(); // generated IV

    console.log(saltHex);
    console.log(ctHex);
    console.log(ivHex);

    // Decrypt
    var bytes = CryptoJS.AES.decrypt(ct.toString(), 'secret key 123', { iv: ct.iv, mode: CryptoJS.mode.CBC });
    var originalText = bytes.toString(CryptoJS.enc.Utf8);

    console.log(originalText);
    youngs
        17
    youngs  
    OP
       2020-04-27 18:11:23 +08:00
    @binkcn 大佬,用 JAVA 怎么取到这个 IV 呢。。。。
    binkcn
        18
    binkcn  
       2020-04-27 18:26:46 +08:00
    @youngs 你前端用 CryptoJS 加密的时候,自己额外存一下本次加密时产生的 iv,回头用 java 解密的时候传进去就行了,这样最简单。

    如果,你现在想解密之前已经加密出来的密文,那么就稍微麻烦一些:

    首先,加密出来的密文是 base64 的,你 base64_decode 之后会发现开头是 “Salted__”这样的,然后紧跟着的 8 个字节就是 salt 了,然后根据我 4 楼的伪代码,Passphrase (即 key ) 和 Salt 你都有了,可以自己算出 iv 。

    这个部分你自己实现即可,18 点过了,我下班回家了…… 2333
    youngs
        19
    youngs  
    OP
       2020-04-27 18:28:10 +08:00
    @binkcn 大佬!我解出来了。。。详细结果 append 到主题上,感谢~
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4429 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 01:04 · PVG 09:04 · LAX 17:04 · JFK 20:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.