基于FPGA的SM3加密算法优化(SM3加密算法三)

24 篇文章 4 订阅
3 篇文章 0 订阅

  前文根据奇哥的方法使用FPGA实现了SM3加密算法,算法实现方式正确,但在并行度为2的情况下,在zynq7030ffg676-2也只能跑到50MHz,并行度为1也跑不到100MHz。

  因此在了解原理的过程中,发现消息扩展和迭代过程其实可以全部放入流水线中,并行度固定为1,最终设计能够跑到200MHz,下面就讲解代码实现过程。

1、代码讲解

  首先是端口信号,根据参数DATA_W设置输入数据位宽,取值范围是[1,447],支持对这个范围内所有数据加密。

module sm3_encrypt #(
    parameter			DATA_W      =       9'd256	             //待加密数据位宽,取值范围[1,447]。
)(
    input									clk		            ,//系统时钟信号;
    input									rst_n 	            ,//系统复位信号,低电平有效;

    input				[DATA_W - 1 : 0]    din		            ,//输入待加密数据;
    input									din_vld	            ,//输入待加密数据有效指示信号,高电平有效;
    output reg			[255 : 0]	        dout	            ,//输出加密结果数据信号;
    output reg								dout_vld             //输出加密结果数据有效指示信号,高电平有效;
);

  然后是整个加密过程中会使用到的函数,如下所示,与前文代码一致。

    //生成置换函数P1
    function [31 : 0] P1;
        input [31 : 0] X;
        begin//X异或X循环左移15位,在异或X循环左移23位;
            P1 = X ^ {X[16:0],X[31:17]} ^ {X[8:0],X[31:9]};
        end
    endfunction

    //计算置换函数P0
    function [31 : 0] P0;
        input [31 : 0] X;
        begin//计算公式:P0=X ^ (X <<< 9) ^ (X <<< 17)
            P0 = X ^ {X[22:0],X[31:23]} ^ {X[14:0],X[31:15]};
        end
    endfunction

    //计算布尔函数FFj(x,y,z);
    function [31 : 0] FFj;
        input [31 : 0] X;
        input [31 : 0] Y;
        input [31 : 0] Z;
        input [31 : 0] j;    
        begin
            FFj = (j < 16) ? (X ^ Y ^ Z) : ((X & Y) | (X & Z) | (Y & Z));
        end
    endfunction

    //计算布尔函数GGj(x,y,z);
    function [31:0] GGj;
        input [31 :0] X;
        input [31 :0] Y;
        input [31 :0] Z;
        input [31 :0] j;    
        begin
            GGj = (j < 16) ? (X ^ Y ^ Z) : ((X & Y) | (~X & Z));
        end
    endfunction

    //常量Tj;
    function [31:0] Tj;
        input [31:0] j;
        begin
            Tj = (j < 16) ? 32'h79cc4519 : 32'h7a879d8a;
        end
    endfunction

  使用移位寄存器将输入数据有效指示信号暂存,与后续填充数据和大端转换数据对齐。

    //使用移位寄存器将输入数据暂存;
    always@(posedge clk)begin
        din_vld_r[2:0] <= {din_vld_r[1:0],din_vld};
    end

  把输入数据填充到512比特,对应代码如下所示。注意工程在运行时必须保证待加密数据位宽与设置的输入数据位宽一致,否则以设置的输入数据位宽为准。

    always@(posedge clk)begin
        if(~rst_n)begin
            padding_data <= 'd0;
        end
        else begin//进行填充,在数据后面加1'b1,然后将数据补足448位,最后64位存储数据长度;
            padding_data <= {din[DATA_W - 1 : 0],1'b1,{{447-DATA_W}{1'b0}},{55'd0,DATA_W[8:0]}};
        end
    end

  数据在扩展和迭代过程中都是以大端模式进行存储,因此需要先将输入小端数据转换为大端数据,对应代码如下所示。

    genvar g_i;
    generate
        for(g_i = 0 ; g_i < 16 ; g_i = g_i + 1)begin : encode
            always@(posedge clk)begin
                if(din_vld_r[0])//将输入的小端数据转换为大端数据;
                    padding_data_big[g_i*32+31 : g_i*32] = padding_data[511-g_i*32 : 480-g_i*32];
            end
        end
    endgenerate

  前面这部分与前文基本一致,修改部分主要在消息扩展,迭代必须每个时钟迭代一轮,因此也没太多修改的地方。

  如下图所示,上面是消息扩展部分,下面是迭代过程,前文是通过一个时钟计算出wi(1667)和wi’(063)所有的结果。经过下面不难看出,每次迭代只使用了消息扩展wi和wi’中的一个数据,那么是不是每个时钟也只需要计算一个wi和wi’的数据即可呢?

  而且消息扩展的大部分延时都在计算wi这部分,那是不是可以在这段计算过程中多插入几级触发器呢?首先wi(016)是消息填充后的数据,表示在后面的64轮迭代过程中,其实只有53个时钟需要计算wi(1667)的值,完全可以插入几级触发器。

在这里插入图片描述

图1 消息扩展和迭代过程

  计算wi’只需要wi和wi+4即可,只需要在迭代开始时,将wi‘0提前计算出即可,后续数据可以在迭代的同时计算出。

  整体的修改思路就是这样,在消息扩展中插入触发器,将一个时钟的并行计算分为64个时钟串行计算,并且不会影响迭代效率。

  对应代码如下所示,首先把大端转换完成的数据存入wi[0~15]中,作为后续运算的数据。

    genvar g_exi;
    generate//将输入的低16个字存入扩展结果中;
        for(g_exi = 0 ; g_exi < 16 ; g_exi = g_exi + 1)begin : Extend_Word1
            always@(posedge clk)begin
                if(~rst_n)
                    extend_data_m[g_exi] <= 'd0;
                else if(din_vld_r[1])
                    extend_data_m[g_exi] <= padding_data_big[(g_exi * 32) + 31 : g_exi * 32];
            end
        end
    endgenerate

  然后计算出wi’[0]便于后续第一轮迭代使用。

    //计算第一个w'(0)
    always@(posedge clk)begin
        if(~rst_n)
            extend_data_m0[0] <= 'd0;
        else if(din_vld_r[2])//将输入的低16个字存入扩展结果中;
            extend_data_m0[0] <= extend_data_m[0] ^ extend_data_m[4];
    end

  将迭代运算的初始值存入ai[0]~hi[0],作为迭代运算的初始值。

    //迭代的初始值;
    always@(posedge clk)begin
        if(~rst_n)begin
            {ai[0],bi[0],ci[0],di[0],ei[0],fi[0],gi[0],hi[0]} <= 256'h7380166f4914b2b9172442d7da8a0600_a96f30bc163138aae38dee4db0fb0e4e;
        end
    end
    

  下面的cmp_vld用于指示当前进行的迭代轮数,对应位为高电平表示正在进行对应轮的迭代。

    //表示正在进行计算;
    always@(posedge clk)begin
        cmp_vld[64:0] <= {cmp_vld[63:0],din_vld_r[2]};
    end

  下面的循环内部包含每个时钟需要完成的消息扩展和迭代运算。

    genvar i;
    generate
        for(i = 0 ; i < 64 ; i = i + 1)begin : I
            //每个时钟完成一次扩展运算;
            if(i < 52)begin
                always@(posedge clk)begin
                    if(~rst_n)
                        p1_x[i] <= 'd0;
                    else if(cmp_vld[i])//用于计算p1(0)~p1(52)的参数;
                        p1_x[i] <= extend_data_m[i] ^ extend_data_m[i+7] ^ {extend_data_m[i+13][16:0],extend_data_m[i+13][31:17]};
                end
            end
            
            //计算P1的结果;
            if((i > 0) && (i < 53))begin
                always@(posedge clk)begin
                    if(~rst_n)
                        p1[i - 1] <= 'd0;
                    else if(cmp_vld[i])//用于计算p1(0)~p1(52)的参数;
                        p1[i - 1] <= P1(p1_x[i - 1]);
                end
            end

            //每个时钟完成一次扩展运算;
            if((i > 1) && (i < 54))begin
                always@(posedge clk)begin
                    if(~rst_n)
                        extend_data_m[14 + i] <= 'd0;
                    else if(cmp_vld[i])//用于计算w(16)~w(67)的数据;
                        extend_data_m[14 + i] <= (p1[i - 2]) ^ {extend_data_m[i+1][24:0],extend_data_m[i+1][31:25]} ^ extend_data_m[i+8];
                end
            end

            //每个时钟完成一次扩展运算;
            if(i < 63)begin
                always@(posedge clk)begin
                    if(~rst_n)
                        extend_data_m0[i + 1] <= 'd0;
                    else if(cmp_vld[i])//用于计算w'(1)~w'(63)的数据;
                        extend_data_m0[i + 1] <= extend_data_m[i + 1] ^ extend_data_m[i + 5];
                end
            end
            /**************************** 消息扩展结束 ****************************/
            
            /**************************** 迭代运算部分开始 ****************************/
            assign w_Tj[i] = Tj(i);//计算Ti
            assign w_Tj_y[i] = (i[4:0]) ? ({w_Tj[i][31 - i[4:0] : 0],w_Tj[i][31:32 - i[4:0]]}) : w_Tj[i];//计算(Ti<<<(jmod32))

            assign w_ss1_mid0[i] = {ai[i][19:0],ai[i][31:20]} + ei[i] + w_Tj_y[i];//计算(A<<<12)+E+(Ti<<<(jmod32));
            assign w_ss1[i] = {w_ss1_mid0[i][24:0] , w_ss1_mid0[i][31:25]};//计算SS1=((A<<<12)+E+(Ti<<<(jmod32)))<<<7;
            assign w_ss2[i] = w_ss1[i] ^ {ai[i][19:0],ai[i][31:20]};//计算SS2=SS1^(A<<<12);
            assign w_tt1[i] = FFj(ai[i],bi[i],ci[i],i) + di[i] + w_ss2[i] + extend_data_m0[i];//计算TT1=FFi(A,B,C)+D+SS2+W';
            assign w_tt2[i] = GGj(ei[i],fi[i],gi[i],i) + hi[i] + w_ss1[i] + extend_data_m[i];//计算TT2=GGi(E,F,G)+H+SS1+Wi;

            always@(posedge clk)begin
                if(~rst_n)
                    {ai[i+1],bi[i+1],ci[i+1],di[i+1],ei[i+1],fi[i+1],gi[i+1],hi[i+1]} <= 'd0;
                else if(cmp_vld[i])//用于计算w'(1)~w'(63)的数据;
                    {ai[i+1],bi[i+1],ci[i+1],di[i+1],ei[i+1],fi[i+1],gi[i+1],hi[i+1]} <= {w_tt1[i],ai[i],{bi[i][22:0],bi[i][31:23]},ci[i],P0(w_tt2[i]),ei[i],{fi[i][12:0],fi[i][31:13]},gi[i]};
            end
            /**************************** 迭代运算部分结束 ****************************/
        end
    endgenerate

  使用三级流水线来计算消息扩展结果wi[16~67],首先计算P1函数的系数,对应代码如下。

    //每个时钟完成一次扩展运算;
    if(i < 52)begin
        always@(posedge clk)begin
            if(~rst_n)
                p1_x[i] <= 'd0;
            else if(cmp_vld[i])//用于计算p1(0)~p1(52)的参数;
                p1_x[i] <= extend_data_m[i] ^ extend_data_m[i+7] ^ {extend_data_m[i+13][16:0],extend_data_m[i+13][31:17]};
        end
    end

  下个时钟将上个时钟得到的p1系数带入函数,计算得到p1函数的结果。

    //计算P1的结果;
    if((i > 0) && (i < 53))begin
        always@(posedge clk)begin
            if(~rst_n)
                p1[i - 1] <= 'd0;
            else if(cmp_vld[i])//用于计算p1(0)~p1(52)的参数;
                p1[i - 1] <= P1(p1_x[i - 1]);
        end
    end

  然后再完成表达式P1 (Wi-16异或Wi-9异或(Wi-3<<<15))异或(Wi-13<<<7)异或Wi-6的计算,对应代码如下。

    //每个时钟完成一次扩展运算;
    if((i > 1) && (i < 54))begin
        always@(posedge clk)begin
            if(~rst_n)
                extend_data_m[14 + i] <= 'd0;
            else if(cmp_vld[i])//用于计算w(16)~w(67)的数据;
                extend_data_m[14 + i] <= (p1[i - 2]) ^ {extend_data_m[i+1][24:0],extend_data_m[i+1][31:25]} ^ extend_data_m[i+8];
        end
    end

  下面是通过wi计算wi’的代码,依旧是采用流水线设计,每个时钟计算一个数据。

    //每个时钟完成一次扩展运算;
    if(i < 63)begin
        always@(posedge clk)begin
            if(~rst_n)
                extend_data_m0[i + 1] <= 'd0;
            else if(cmp_vld[i])//用于计算w'(1)~w'(63)的数据;
                extend_data_m0[i + 1] <= extend_data_m[i + 1] ^ extend_data_m[i + 5];
        end
    end

  上述完成了消息扩展,下面就是迭代的内容,由于迭代必须在一个时钟完成,而且里面的函数需用到上次迭代的结果,导致中间无法插入触发器(插入触发器会导致几个时钟才能完成一次迭代,会减小数据吞吐量),因此这部分依旧使用组合逻辑实现,与前文并没有什么改变。

  下面是计算ss1、ss2、tt1、tt2的代码,直接与图1的迭代过程对比即可。

    assign w_Tj[i] = Tj(i);//计算Ti
    assign w_Tj_y[i] = (i[4:0]) ? ({w_Tj[i][31 - i[4:0] : 0],w_Tj[i][31:32 - i[4:0]]}) : w_Tj[i];//计算(Ti<<<(jmod32))

    assign w_ss1_mid0[i] = {ai[i][19:0],ai[i][31:20]} + ei[i] + w_Tj_y[i];//计算(A<<<12)+E+(Ti<<<(jmod32));
    assign w_ss1[i] = {w_ss1_mid0[i][24:0] , w_ss1_mid0[i][31:25]};//计算SS1=((A<<<12)+E+(Ti<<<(jmod32)))<<<7;
    assign w_ss2[i] = w_ss1[i] ^ {ai[i][19:0],ai[i][31:20]};//计算SS2=SS1^(A<<<12);
    assign w_tt1[i] = FFj(ai[i],bi[i],ci[i],i) + di[i] + w_ss2[i] + extend_data_m0[i];//计算TT1=FFi(A,B,C)+D+SS2+W';
    assign w_tt2[i] = GGj(ei[i],fi[i],gi[i],i) + hi[i] + w_ss1[i] + extend_data_m[i];//计算TT2=GGi(E,F,G)+H+SS1+Wi;

  后面是将迭代的计算结果存入ai[i+1]~hi[i+1]中,作为下轮迭代的初始值。

    always@(posedge clk)begin
        if(~rst_n)
            {ai[i+1],bi[i+1],ci[i+1],di[i+1],ei[i+1],fi[i+1],gi[i+1],hi[i+1]} <= 'd0;
        else if(cmp_vld[i])//用于计算w'(1)~w'(63)的数据;
            {ai[i+1],bi[i+1],ci[i+1],di[i+1],ei[i+1],fi[i+1],gi[i+1],hi[i+1]} <= {w_tt1[i],ai[i],{bi[i][22:0],bi[i][31:23]},ci[i],P0(w_tt2[i]),ei[i],{fi[i][12:0],fi[i][31:13]},gi[i]};
    end

  将64轮迭代的结果ai[64]~hi[64]与迭代初始值ai[0]~hi[0]异或得到加密结果,对应代码如下,同时生成加密结果有效指示信号。

    always@(posedge clk)begin
        if(~rst_n)begin
            dout  <= 'd0;
        end
        else begin//将迭代计算的最终结果与初始值异或,得到加密结果;
            dout  <= {ai[64],bi[64],ci[64],di[64],ei[64],fi[64],gi[64],hi[64]} ^ {ai[0],bi[0],ci[0],di[0],ei[0],fi[0],gi[0],hi[0]};
        end
    end

    always@(posedge clk)begin
        dout_vld <= cmp_vld[64];
    end

2、代码仿真

  顶层模块和仿真激励文件生成模块依旧使用前文的代码,只需要修改模块的参数即可。

  顶层模块代码如下所示:

//--###############################################################################################
//--#
//--# File Name		: top
//--# Designer		: 数字站
//--# Tool			: Vivado 2021.1
//--# Design Date	: 2024.6.2
//--# Description	: sm3加密顶层模块
//--# Version		: 0.0
//--# Coding scheme	: GBK(If the Chinese comment of the file is garbled, please do not save it and check whether the file is opened in GBK encoding mode)
//--#
//--###############################################################################################
module top (
    input			        clk		        ,//系统时钟信号,默认100MHz;
    input			        rst_n	        ,//系统复位信号,低电平有效;
    output   reg            led             
);
    (* MARK_DEBUG = "TRUE" *)reg     [255 : 0]       Original_Data   ;
    (* MARK_DEBUG = "TRUE" *)reg                     Original_Valid  ;
    reg     [3  : 0]       cnt             ;

    (* MARK_DEBUG = "TRUE" *)wire    [255 : 0]       Encrypt_Data    ;
    (* MARK_DEBUG = "TRUE" *)wire                    Encrypt_Valid   ;

    sm3_encrypt #(
        .DATA_W         ( 256            ) //待加密数据位宽,取值范围[1,447]。
    )
    u_sm3_encrypt (
        .clk	        ( clk           ),//系统时钟信号;
        .rst_n 	        ( rst_n         ),//系统复位信号,低电平有效;
        .din	        ( Original_Data ),//24'h616263    ),//输入待加密数据;
        .din_vld        ( Original_Valid),//输入待加密数据有效指示信号,高电平有效;
        .dout	        ( Encrypt_Data  ),//输出加密结果数据信号;
        .dout_vld       ( Encrypt_Valid ) //输出加密结果数据有效指示信号,高电平有效;
    );

    //循环产生测试数据的时钟信号;
    always@(posedge clk)begin
        if(~rst_n)
            cnt <= 'd0;
        else 
            cnt <= cnt + 'd1;
    end

    //每间隔128个时钟生成一个测试数据;
    always@(posedge clk)begin
        if(~rst_n)begin
            Original_Data  <= 256'h0001020304050607_08090a0b0c0d0e0f_0001020304050607_08090a0b0c0d0e0f;
            Original_Valid <= 'd0;
        end 
        else if(&cnt)begin
            Original_Data  <= Original_Data + 3;
            Original_Valid <= 'd1;
        end 
        else begin
            Original_Valid <= 'd0;
        end
    end

    //防止Encrypt_Data被优化掉;
    always@(posedge clk)begin
        if(~rst_n)begin//初始值为0;
            led <= 'd0;
        end
        else if(Encrypt_Valid)begin
            led <= (Encrypt_Data > 10000);
        end
    end

endmodule

  运行仿真结果如下所示,加密数据为256’h0001020304050607_08090a0b0c0d0e0f_0001020304050607_08090a0b0c0d0e12,最后加密结果为256’h3C6F866ABC77AF25_F3EE32C4864BDAE506_F4A946197D774D45_ACD127797360A6。

在这里插入图片描述

图2 仿真结果

  使用网页加密的结果如下所示,与上图仿真保持一致,证明上述设计的逻辑运算应该没有问题。

在这里插入图片描述

图3 网页加密

3、上板测试

  首先综合工程,然后分配管脚,添加ILA,加入时钟约束,当前系统时钟约束为200MHz,如下图所示,然后对工程进行布局布线,最后生成比特流文件。

在这里插入图片描述

图4 时钟约束

  查看时序报告,如下所示,建立时间余量和保持时间余量均大于零,表示该设计工作在200MHz时钟下没有问题。

在这里插入图片描述

图5 时序报告

  将比特流文件下载到开发板,使用ILA抓取一帧数据,如下所示。待加密数据为256’h0001020304050607_08090a0b0c0d0e0f_0001020 304050607_08090a0b0d0856a3,抓取加密结果为256’h 2FE2A06A9075CC30_30BA74879761D179_60AD30C6ABA57F37_ECA4410AA A1E0864。

在这里插入图片描述

图6 ILA抓取加密时序

  使用加密工具验证结果如下所示,与上图加密结果一致,表示加密正常。

在这里插入图片描述

图7 网页加密结果

  使用ILA再抓取一帧数据,如下所示。待加密数据为256’h0001020304050607_08090a0b0c0d0e0f_0001020304050607_08090a0 b341f75ca,抓取加密结果为256’h D9E6BCD11C87679B_D07B230BC9368229_6537E97CFD774AAD_A54ABDAEBA935C1B。

在这里插入图片描述

图8 ILA抓取加密时序

  使用加密工具验证结果如下所示,与上图加密结果一致,表示加密正常。

在这里插入图片描述

图9 网页加密结果

  在讲解SM3原理时,对24’h616263进行了加密,本文对该加密结果进行验证,首先把顶层模块的输入数据位宽修改为24位,输入数据改为固定的24’h616263,如下图所示。

在这里插入图片描述

图10 修改加密输入数据

  然后重新综合工程,将生成的比特流下载到FPGA中,使用ILA抓取加密结果如下所示,加密结果为256’h 66C7F0F462EEEDD9_D1F2 D46BDC10E4E2_4167C4875CF2F7A2_297DA02B8F4BA8E0。

在这里插入图片描述

图11 24’h616263的加密结果

  最后使用网页工具验证24’h616263的加密结果,如下所示,与上图ILA抓取的结果一致,表示加密正确,与前文原理讲解时的加密结果也是一致的。

在这里插入图片描述

图12 网页加密结果

  虽然该工程在zynq7030ffg676-2上可以正常工作在200MHz时钟下,但是输入有效数据间隔时间必须大于等于16个时钟,因为消息扩展的低16个数据必须被用于迭代计算后才能输入新的数据,否则这些数据会被覆盖掉,导致计算错误。

  通过把消息扩展的低16个数据使用触发器保存一段时间,不会被后输入的数据覆盖,来达到支持连续数据输入的目的。但是会消耗更多的资源,奇哥的代码由于消息扩展在一个时钟就被计算出来,然后保存了,因此下一个数据输入不会影响上次输入的数据计算。

  本文的工程可以在公众号后台回复“SM3算法的流水线优化”(不包括引号)获取。


  如果对文章内容理解有疑惑或者对代码不理解,可以在评论区或者后台留言,看到后均会回复!

  如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!您的支持是我更新的最大动力!将持续更新工程!

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
SM4加密算法可以在FPGA上实现,具体可以采用循环架构或者流水线架构。循环架构针对资源节约进行优化,适用于资源受限的硬件设备。流水线架构则针对加密性能进行优化,适用于对吞吐量有较高要求的场景。在循环架构中,每个消息分组的加密过程需要32个时钟周期。SM4算法的硬件设计通常包括密钥扩展算法、加密算法和解密算法。加密和解密算法结构相同,只是轮密钥使用的顺序相反。SM4的分组长度和密钥长度都是128位(即16字节,4字)。加密过程包括32轮迭代和一次反序变换,解密过程与加密过程完全相同,只是轮密钥的使用顺序相反 [1 [2。 需要注意的是,在FPGA上实现SM4加密算法需要相应的硬件支持和编程能力。一种实现方式是使用HDL语言如Verilog或VHDL编写SM4算法的硬件描述,并通过综合、布局布线和生成比特流等步骤将其烧录到FPGA芯片中。另一种方式是使用现有的FPGA开发板,如Xilinx或Altera的开发板,利用它们提供的开发工具和资源来实现SM4算法。具体的实现方式还需根据具体的硬件平台和需求进行调整和优化。 综上所述,SM4加密算法可以在FPGA上实现,并且可以根据具体需求选择适合的架构和实现方式 [1 [2。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [SM4分组密码算法的verilog实现(附免费可用代码)](https://blog.csdn.net/weixin_43261410/article/details/125153796)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [SM4 加密算法](https://blog.csdn.net/weixin_46455069/article/details/122991627)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [[极简教学]Java的SM3加密算法(附GitHub源码教学)](https://blog.csdn.net/qq_41579123/article/details/106571243)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

电路_fpga

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值