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

与银行对接 sm4 国密算法

  •  
  •   cbasil · 8 天前 · 3052 次点击
    最近跟银行对接 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)来解密。
    20 条回复    2024-11-01 14:27:19 +08:00
    momo7411
        1
    momo7411  
       8 天前 via Android
    iv 固定?
    bagel
        2
    bagel  
       8 天前   ❤️ 5
    IV 写死不用看了,屎山加屎而已,至于对不对,根本无人在意。
    joyhub2140
        3
    joyhub2140  
       8 天前
    iv 写死,哈哈,那每次加密的密文是不是都一样?
    hervey0424
        4
    hervey0424  
       8 天前
    直接用他们的开发语言弄个中间层比研究这玩意强多了
    cbasil
        5
    cbasil  
    OP
       8 天前
    @bagel 银行加密的 IV 就是默认填 0 生成的,你如果用随机数生成 iv ,解密肯定有问题。至于全零 IV 生成,用 str_repeat("\0", 16)更简洁更明确。
    xshanow
        7
    xshanow  
       8 天前
    @cbasil SM 系列一般要用硬件产品的才合规,我们专门做这类产品的
    InkStone
        8
    InkStone  
       8 天前
    @xshanow 有软认证。不过只能到二级
    majula
        9
    majula  
       8 天前
    @momo7411 @bagel @joyhub2140

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

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

    有的时候甚至不得不这样做,尤其是在没有可靠的 random source 可用的时候(比如一些嵌入式场景)
    ca2oh4
        10
    ca2oh4  
       8 天前
    php 对接属实有点困难,国密那一套一开始好像是国内的区块链研究机构搞的.记得 有发布官方的 sdk 来着(golang 版本)
    YUCOAT
        11
    YUCOAT  
       8 天前
    我以前搞国密的时候,用到了 GmSSL ,你可以参考一下
    dode
        12
    dode  
       8 天前
    用 Java 包做,bouncycastle
    ntedshen
        13
    ntedshen  
       8 天前
    话说我这里有个阿里的 javasdk 用的个 RSA/ECB/OAEPPadding ,似乎完全找不到其他语言的方案。。。
    找 ai 要了几个 nodejs 的包但是实际上加密加不出来。。。
    现在专门跑了个 tomcat 当接口用着的。。。
    cbasil
        14
    cbasil  
    OP
       8 天前
    @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  
       8 天前
    这块都一样,国密和国际算法都是这些坑,
    我公司几年前做接口加固时我写文档教其他各端实现就踩了这种坑, 家家有本难念的经,尤其 js 连“字节数组”的概念都没有,整数类型也没有字节数的概念,我都很难给他们解释,
    最终文档里是单开一页用来举例子,就是涉及到的每种加密算法封装后的输入输出给个具体例子参考,涉及字节数组就强调该字节数组 base64 编码或者 16 进制编码后是某某某,

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