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

与银行对接 sm4 国密算法

  •  
  •   cbasil · 21 小时 14 分钟前 · 2441 次点击
    最近跟银行对接 sm4 国密算法,搞了好久才对接好,我大概讲一下开发中遇到的坑。
    php 相关国密算法的教程很少,找了好久才找到 github 上的包[https://github.com/lizhichao/sm]( https://github.com/lizhichao/sm)
    配置好后,一运行就报错秘钥长度为 16 位。跟对方沟通后才知道他们给的是 16 进制的 32 位的 key,在 php 中需要用 hex2bin 转成 16 位。
    如果对方给的加密模式是 sm4-cbc,还需要配置 iv 。默认 iv 用 hex2bin('00000000000000000000000000000000')生成。不然解密后的字符串前后会有乱码。填充方法一般常用 pkcs5 和 pkcs7 。这二种填充方式概念上没有什么区别,只是 pkcs5 在 blockSize 上固定为 8 bytes,即数据始终会被切割成 8 个字节的数据块,然后计算需要填充的长度.
    加密后的字符编码也有 hex 和 base64 区分。

    如果 openssl 版本大于 1.1.1 ,就可以用 openssl_decrypt($data, "sm4", $key, $options=OPENSSL_RAW_DATA,$iv)来解密。
    19 条回复    2024-10-31 23:08:17 +08:00
    momo7411
        1
    momo7411  
       21 小时 3 分钟前 via Android
    iv 固定?
    bagel
        2
    bagel  
       20 小时 58 分钟前   ❤️ 5
    IV 写死不用看了,屎山加屎而已,至于对不对,根本无人在意。
    joyhub2140
        3
    joyhub2140  
       20 小时 31 分钟前
    iv 写死,哈哈,那每次加密的密文是不是都一样?
    hervey0424
        4
    hervey0424  
       20 小时 14 分钟前
    直接用他们的开发语言弄个中间层比研究这玩意强多了
    cbasil
        5
    cbasil  
    OP
       19 小时 36 分钟前
    @bagel 银行加密的 IV 就是默认填 0 生成的,你如果用随机数生成 iv ,解密肯定有问题。至于全零 IV 生成,用 str_repeat("\0", 16)更简洁更明确。
    PendingOni
        6
    PendingOni  
       19 小时 6 分钟前
    xshanow
        7
    xshanow  
       18 小时 55 分钟前
    @cbasil SM 系列一般要用硬件产品的才合规,我们专门做这类产品的
    InkStone
        8
    InkStone  
       18 小时 53 分钟前
    @xshanow 有软认证。不过只能到二级
    majula
        9
    majula  
       18 小时 51 分钟前
    @momo7411 @bagel @joyhub2140

    固定 iv 是可行的,只要你确保第一个 block 的明文不重复(比如用一个自增 id ),且确保其无法被攻击者自由选择即可。这时,第一个 block 的密文等效于一个用 CSPRNG 生成的 iv

    NIST SP 800-38A 官方支持了这种用法(见 Appendix C )

    有的时候甚至不得不这样做,尤其是在没有可靠的 random source 可用的时候(比如一些嵌入式场景)
    ca2oh4
        10
    ca2oh4  
       18 小时 39 分钟前
    php 对接属实有点困难,国密那一套一开始好像是国内的区块链研究机构搞的.记得 有发布官方的 sdk 来着(golang 版本)
    YUCOAT
        11
    YUCOAT  
       18 小时 36 分钟前
    我以前搞国密的时候,用到了 GmSSL ,你可以参考一下
    dode
        12
    dode  
       18 小时 18 分钟前
    用 Java 包做,bouncycastle
    ntedshen
        13
    ntedshen  
       17 小时 59 分钟前
    话说我这里有个阿里的 javasdk 用的个 RSA/ECB/OAEPPadding ,似乎完全找不到其他语言的方案。。。
    找 ai 要了几个 nodejs 的包但是实际上加密加不出来。。。
    现在专门跑了个 tomcat 当接口用着的。。。
    cbasil
        14
    cbasil  
    OP
       17 小时 17 分钟前
    @ca2oh4 我当时也考虑用 golang 写一个脚本,然后 php 通过 http 调用。不过后面解决了就不用了。这是当时写的 golang 案例

    ```golang
    package main

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

    "github.com/tjfoc/gmsm/sm4"
    )

    // PKCS5Padding 使用 PKCS5 填充
    func PKCS5Padding(src []byte, blockSize int) []byte {
    padding := blockSize - len(src)%blockSize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(src, padtext...)
    }

    // PKCS5UnPadding 去除 PKCS5 填充
    func PKCS5UnPadding(src []byte) []byte {
    length := len(src)
    unpadding := int(src[length-1])
    return src[:(length - unpadding)]
    }

    // SM4 CBC 模式加密
    func sm4CBCEncrypt(key, plaintext, iv []byte) ([]byte, error) {
    block, err := sm4.NewCipher(key)
    if err != nil {
    return nil, err
    }

    plaintext = PKCS5Padding(plaintext, block.BlockSize())
    ciphertext := make([]byte, len(plaintext))
    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(ciphertext, plaintext)
    return ciphertext, nil
    }

    // SM4 CBC 模式解密
    func sm4CBCDecrypt(key, ciphertext, iv []byte) ([]byte, error) {
    block, err := sm4.NewCipher(key)
    if err != nil {
    return nil, err
    }

    plaintext := make([]byte, len(ciphertext))
    mode := cipher.NewCBCDecrypter(block, iv)
    mode.CryptBlocks(plaintext, ciphertext)
    plaintext = PKCS5UnPadding(plaintext)
    return plaintext, nil
    }

    func main() {
    key, _ := hex.DecodeString("key") // 16 字节的十六进制密钥
    iv, _ := hex.DecodeString("iv") // 16 字节的 IV
    plaintext := []byte("This is a secret message.")

    // 加密
    ciphertext, err := sm4CBCEncrypt(key, plaintext, iv)
    if err != nil {
    fmt.Println("Encryption error:", err)
    return
    }
    fmt.Printf("Ciphertext (hex): %s\n", hex.EncodeToString(ciphertext))
    // 解密
    decrypted, err := sm4CBCDecrypt(key, ciphertext, iv)
    if err != nil {
    fmt.Println("Decryption error:", err)
    return
    }
    fmt.Printf("Decrypted text: %s\n", decrypted)
    }
    ```
    AoEiuV020JP
        15
    AoEiuV020JP  
       17 小时 7 分钟前
    这块都一样,国密和国际算法都是这些坑,
    我公司几年前做接口加固时我写文档教其他各端实现就踩了这种坑, 家家有本难念的经,尤其 js 连“字节数组”的概念都没有,整数类型也没有字节数的概念,我都很难给他们解释,
    最终文档里是单开一页用来举例子,就是涉及到的每种加密算法封装后的输入输出给个具体例子参考,涉及字节数组就强调该字节数组 base64 编码或者 16 进制编码后是某某某,

    但后面其他同事设计别的加密时还是会做出比如 长度 16 的字节数组,先 base64 编码成长度 24 的字符串再截取 16 字符转成 新的 16 字节作为密钥使用,这种意义不明麻烦还降低安全性的操作,但一开始定好了这一套操作后面别人实现就都得做成一样的,
    GiggleSmile
        16
    GiggleSmile  
       16 小时 35 分钟前
    @AoEiuV020JP 说得很对。
    cbasil
        17
    cbasil  
    OP
       16 小时 17 分钟前   ❤️ 1
    @ntedshen RSA 也是一个大坑,之前对接的一个项目,接口用到私钥签名、公钥验签加上公钥加密、私钥解密。双方交换公钥。折腾了好久,发现 rsa 加密要分段加密。RSA 密钥长度 1024bit ,加密的时候 117 个字符加密一次,然后把所有的密文拼接成一个密文;解密的时候需要 128 个字符解密一下,然后拼接成数据。具体可以看看这篇文章 https://www.cnblogs.com/meetuj/p/14954533.html
    不同语言的加解密处理确实太麻烦了,尤其是对方一句话,我们用的是默认的加密方式。你们自己实现就好了。代码也不给,给一串加密前和加密后的参数。你自己慢慢去试。成功了就是精诚所至金石为开。
    brando
        18
    brando  
       15 小时 6 分钟前
    这种我一律要 SDK 的,没这标准咋搞,靠理解还是猜来猜去?
    momo7411
        19
    momo7411  
       9 小时 57 分钟前
    @majula 你这里提到用一个自增 id 的密文来等效充当 iv ,那么自增 id 的初始值是不是也得随机生成?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4509 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 01:05 · PVG 09:05 · LAX 18:05 · JFK 21:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.