github.com/emmansun/gmsm@v0.29.1/docs/sm4.md (about)

     1  # SM4分组密码算法应用指南
     2  ## 参考标准
     3  * 《GB/T 32907-2016 信息安全技术 SM4分组密码算法》
     4  * 《GB/T 17964-2021 信息安全技术 分组密码算法的工作模式》
     5  
     6  您可以从[国家标准全文公开系统](https://openstd.samr.gov.cn/)在线阅读这些标准。
     7  
     8  ## 概述
     9  SM4分组密码算法,其地位类似NIST中的AES分组密码算法,密钥长度128位(16字节),分组大小也是128位(16字节)。在本软件库中,SM4的实现与Go语言中的AES实现一致,也实现了```cipher.Block```接口,所以,所有Go语言中实现的工作模式(CBC/GCM/CFB/OFB/CTR),都能与SM4组合使用。
    10  
    11  ## [工作模式](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation)
    12  Go语言实现的工作模式,主要有三类:
    13  * 基于分组的工作模式 ```cipher.BlockMode```,譬如CBC。
    14  * 带有关联数据的认证加密工作模式```cipher.AEAD```,譬如GCM。
    15  * 流加密工作模式```cipher.Stream```,譬如CTR、CFB、OFB。
    16  
    17  在实际加解密操作中,我们一般不会直接使用```cipher.Block```,必须结合分组密码算法的工作模式使用。除了Go语言自带的工作模式(CBC/GCM/CFB/OFB/CTR),本软件库也实现了下列工作模式:
    18  * ECB - 电码本模式
    19  * BC - 分组链接模式
    20  * HCTR - 带泛杂凑函数的计数器模式
    21  * XTS - 带密文挪用的XEX可调分组密码模式
    22  * OFBNLF - 带非线性函数的输出反馈模式
    23  * CCM - 分组密码链接-消息认证码组合模式
    24  
    25  其中,ECB/BC/HCTR/XTS/OFBNLF是《GB/T 17964-2021 信息安全技术 分组密码算法的工作模式》列出的工作模式。BC/OFBNLF模式是商密中的遗留工作模式,**不建议**在新的应用中使用。XTS/HCTR模式适用于对磁盘加密,其中HCTR模式是《GB/T 17964-2021 信息安全技术 分组密码算法的工作模式》最新引入的,HCTR模式最近业界研究比较多,也指出了原论文中的Bugs:On modern processors HCTR [WFW05](https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.470.5288) is one of the most efficient constructions for building a tweakable super-pseudorandom permutation. However, a bug in the specification and another in Chakraborty and Nandi’s security proof [CN08](https://www.iacr.org/cryptodb/archive/2008/FSE/paper/15611.pdf) invalidate the claimed security bound.  
    26  不知道这个不足是否会影响到这个工作模式的采用。很奇怪《GB/T 17964-2021 信息安全技术 分组密码算法的工作模式》为何没有纳入GCM工作模式,难道是版权问题?
    27  
    28  本软件库引入CCM模式,只是为了有些标准还用到该模式。ECB模式也不建议单独使用。
    29  
    30  目前,本软件库的SM4针对ECB/CBC/GCM/XTS工作模式进行了绑定组合性能优化,暂时没有计划使用汇编优化HCTR模式(HCTR模式可以采用和GCM类似的方法进行汇编优化)。
    31  
    32  ### 使用建议
    33  常用的对称加解密应用场合,推荐优先使用GCM模式,其次CBC模式(一些安全扫描工具,也会把CBC工作模式列为安全性不高的工作模式)。我能想到的GCM模式的缺点是:加解密的相关方不支持GCM模式,或者实现性能不好。
    34  
    35  
    36  ## 填充(padding)
    37  有些分组密码算法的工作模式(譬如实现了```cipher.BlockMode```接口的模式)的输入要求是其长度必须是分组大小的整数倍。《GB/T 17964-2021 信息安全技术 分组密码算法的工作模式》附录C中列出了以下几种填充模式:
    38  * 填充方式 1,对应本软件库的```padding.NewPKCS7Padding```
    39  * 填充方式 2,对应本软件库的```padding.NewISO9797M2Padding```
    40  * 填充方式 3,目前没有实现,它对应ISO/IEC_9797-1 padding method 3
    41  
    42  本软件库也实现了ANSI X9.23标准中定义的填充方式```padding.NewANSIX923Padding```,**用的最广的还是填充方式 1:PKCS7填充**。
    43  
    44  您如果使用实现了```cipher.BlockMode```接口的分组加密工作模式,那您也必须与相关方协调好填充模式。JAVA库的对称加密算法字符串名就包含了所有信息,譬如**AES/CBC/PKCS7Padding**。
    45  
    46  ## 密文及其相关参数的传输和存储
    47  如果是自描述的,那肯定有相关标准,定义相关ASN.1结构,并且给分组密码算法、工作模式、填充方式都赋予一个OID。或者如hashicorp vault,一个对称密钥确定了分组密码算法、工作模式、填充方式,最终输出密文是密钥ID和原始密文的组合。
    48  
    49  如果是内部服务之间,可能是在应用/服务级别自定义所使用分组密码算法、工作模式、填充方式的标识,作为应用的METADATA,也就是加密用的METADATA和密文分离。
    50  
    51  也可能是隐式使用一致的分组密码算法、工作模式、填充方式,也就是代码知道,还有文档知道?
    52  
    53  具体使用哪种方式,取决于应用场景。
    54  
    55  另外一个就是必须和密文一起存储/传输的参数,譬如,如果使用CBC工作模式,那IV怎么办?如果是GCM模式,那Nonce、Nonce长度、Tag长度怎么办?这通常也有两种方案:
    56  * 使用预定义的ASN.1结构
    57  * 和密文简单拼接:譬如CBC工作模式:前面16字节IV,后面ciphertext;GCM模式(使用默认Tag长度和Nonce长度):前面12字节Nonce,后面ciphertext。
    58  
    59  至于要将二进制转为文本传输、存储,编个码就行:标准base64 / URL base64 / HEX,事先协调、定义好就可以了。这里顺便推荐一下[性能更好的BASE64实现](https://github.com/emmansun/base64)。
    60  
    61  ## API文档及示例
    62  这里只列出GCM/CBC的例子,其余请参考[API Document](https://godoc.org/github.com/emmansun/gmsm)。
    63  
    64  ### GCM示例
    65  ```go
    66  func Example_encryptGCM() {
    67  	// Load your secret key from a safe place and reuse it across multiple
    68  	// Seal/Open calls. (Obviously don't use this example key for anything
    69  	// real.) If you want to convert a passphrase to a key, use a suitable
    70  	// package like bcrypt or scrypt.
    71  	key, _ := hex.DecodeString("6368616e676520746869732070617373")
    72  	plaintext := []byte("exampleplaintext")
    73  
    74  	block, err := sm4.NewCipher(key)
    75  	if err != nil {
    76  		panic(err.Error())
    77  	}
    78  
    79  	// Never use more than 2^32 random nonces with a given key because of the risk of a repeat.
    80  	nonce := make([]byte, 12)
    81  	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
    82  		panic(err.Error())
    83  	}
    84  
    85  	sm4gcm, err := cipher.NewGCM(block)
    86  	if err != nil {
    87  		panic(err.Error())
    88  	}
    89  
    90  	// You can encode the nonce and ciphertext with your own scheme
    91  	ciphertext := sm4gcm.Seal(nil, nonce, plaintext, nil)
    92  	fmt.Printf("%x %x\n", nonce, ciphertext)
    93  }
    94  
    95  func Example_decryptGCM() {
    96  	// Load your secret key from a safe place and reuse it across multiple
    97  	// Seal/Open calls. (Obviously don't use this example key for anything
    98  	// real.) If you want to convert a passphrase to a key, use a suitable
    99  	// package like bcrypt or scrypt.
   100  	key, _ := hex.DecodeString("6368616e676520746869732070617373")
   101  	// You can decode the nonce and ciphertext with your encoding scheme
   102  	ciphertext, _ := hex.DecodeString("b7fdece1c6b3dce9cc386e8bc93df0ce496df789166229f14b973b694a4a23c3")
   103  	nonce, _ := hex.DecodeString("07d168e0517656ab7131f495")
   104  
   105  	block, err := sm4.NewCipher(key)
   106  	if err != nil {
   107  		panic(err.Error())
   108  	}
   109  
   110  	sm4gcm, err := cipher.NewGCM(block)
   111  	if err != nil {
   112  		panic(err.Error())
   113  	}
   114  
   115  	plaintext, err := sm4gcm.Open(nil, nonce, ciphertext, nil)
   116  	if err != nil {
   117  		panic(err.Error())
   118  	}
   119  
   120  	fmt.Printf("%s\n", plaintext)
   121  	// Output: exampleplaintext
   122  }
   123  ```
   124  
   125  ### CBC示例
   126  ```go
   127  func Example_encryptCBC() {
   128  	// Load your secret key from a safe place and reuse it across multiple
   129  	// NewCipher calls. (Obviously don't use this example key for anything
   130  	// real.) If you want to convert a passphrase to a key, use a suitable
   131  	// package like bcrypt or scrypt.
   132  	key, _ := hex.DecodeString("6368616e676520746869732070617373")
   133  	plaintext := []byte("sm4 exampleplaintext")
   134  
   135  	block, err := sm4.NewCipher(key)
   136  	if err != nil {
   137  		panic(err)
   138  	}
   139  
   140  	// CBC mode works on blocks so plaintexts may need to be padded to the
   141  	// next whole block. For an example of such padding, see
   142  	// https://tools.ietf.org/html/rfc5246#section-6.2.3.2.
   143  	pkcs7 := padding.NewPKCS7Padding(sm4.BlockSize)
   144  	paddedPlainText := pkcs7.Pad(plaintext)
   145  
   146  	// The IV needs to be unique, but not secure. Therefore it's common to
   147  	// include it at the beginning of the ciphertext.
   148  	ciphertext := make([]byte, sm4.BlockSize+len(paddedPlainText))
   149  	iv := ciphertext[:sm4.BlockSize]
   150  	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
   151  		panic(err)
   152  	}
   153  
   154  	mode := cipher.NewCBCEncrypter(block, iv)
   155  	mode.CryptBlocks(ciphertext[sm4.BlockSize:], paddedPlainText)
   156  
   157  	fmt.Printf("%x\n", ciphertext)
   158  }
   159  
   160  func Example_decryptCBC() {
   161  	// Load your secret key from a safe place and reuse it across multiple
   162  	// NewCipher calls. (Obviously don't use this example key for anything
   163  	// real.) If you want to convert a passphrase to a key, use a suitable
   164  	// package like bcrypt or scrypt.
   165  	key, _ := hex.DecodeString("6368616e676520746869732070617373")
   166  	ciphertext, _ := hex.DecodeString("4d5a1486bfda1b34447afd5bb852e77a867cc6b726a8a0e0ef9b2c21fffc3a30b42acf504628f65cb3fba339101c98ff")
   167  
   168  	block, err := sm4.NewCipher(key)
   169  	if err != nil {
   170  		panic(err)
   171  	}
   172  
   173  	// The IV needs to be unique, but not secure. Therefore it's common to
   174  	// include it at the beginning of the ciphertext.
   175  	if len(ciphertext) < sm4.BlockSize {
   176  		panic("ciphertext too short")
   177  	}
   178  	iv := ciphertext[:sm4.BlockSize]
   179  	ciphertext = ciphertext[sm4.BlockSize:]
   180  
   181  	mode := cipher.NewCBCDecrypter(block, iv)
   182  
   183  	// CryptBlocks can work in-place if the two arguments are the same.
   184  	mode.CryptBlocks(ciphertext, ciphertext)
   185  
   186  	// Unpad plaintext
   187  	pkcs7 := padding.NewPKCS7Padding(sm4.BlockSize)
   188  	ciphertext, err = pkcs7.Unpad(ciphertext)
   189  	if err != nil {
   190  		panic(err)
   191  	}
   192  
   193  	fmt.Printf("%s\n", ciphertext)
   194  	// Output: sm4 exampleplaintext
   195  }
   196  ```
   197  
   198  需要注意一下,```cipher.AEAD```对```dst```参数的要求:
   199  
   200  ```cipher.AEAD```是**追加**结果,所以如果要重用切片,要注意一下。而且```Seal```的结果要比plaintext长(加上tag),所以只有```cap(plaintext)>=len(plaintext)+tagSize```时才会重用,否则还是会新建一个切片。
   201  ```go
   202  // AEAD is a cipher mode providing authenticated encryption with associated
   203  // data. For a description of the methodology, see
   204  // https://en.wikipedia.org/wiki/Authenticated_encryption.
   205  type AEAD interface {
   206  	// NonceSize returns the size of the nonce that must be passed to Seal
   207  	// and Open.
   208  	NonceSize() int
   209  
   210  	// Overhead returns the maximum difference between the lengths of a
   211  	// plaintext and its ciphertext.
   212  	Overhead() int
   213  
   214  	// Seal encrypts and authenticates plaintext, authenticates the
   215  	// additional data and appends the result to dst, returning the updated
   216  	// slice. The nonce must be NonceSize() bytes long and unique for all
   217  	// time, for a given key.
   218  	//
   219  	// To reuse plaintext's storage for the encrypted output, use plaintext[:0]
   220  	// as dst. Otherwise, the remaining capacity of dst must not overlap plaintext.
   221  	Seal(dst, nonce, plaintext, additionalData []byte) []byte
   222  
   223  	// Open decrypts and authenticates ciphertext, authenticates the
   224  	// additional data and, if successful, appends the resulting plaintext
   225  	// to dst, returning the updated slice. The nonce must be NonceSize()
   226  	// bytes long and both it and the additional data must match the
   227  	// value passed to Seal.
   228  	//
   229  	// To reuse ciphertext's storage for the decrypted output, use ciphertext[:0]
   230  	// as dst. Otherwise, the remaining capacity of dst must not overlap plaintext.
   231  	//
   232  	// Even if the function fails, the contents of dst, up to its capacity,
   233  	// may be overwritten.
   234  	Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error)
   235  }
   236  ```
   237  而```cipher.BlockMode```和```cipher.Stream```的话,则是直接覆盖。
   238  
   239  ## 性能
   240  SM4分组密码算法的软件高效实现,不算CPU指令支持的话,已知有如下几种方法:
   241  * S盒和L转换预计算,本软件库纯Go语言实现采用该方法
   242  * SIMD并行处理:并行查表
   243  * SIMD并行处理:借助CPU的AES指令,本软件库采用该方法
   244  * SIMD并行处理:位切片(bitslicing),[参考实现](https://github.com/emmansun/sm4bs)
   245  
   246  当然,这些与有CPU指令支持的AES算法相比,性能差距依然偏大,要是工作模式不支持并行,差距就更巨大了。
   247  
   248  ### 混合方式
   249  从**v0.25.0**开始,AMD64/ARM64 支持AES-NI的CPU架构下,**默认会使用混合方式**,即```cipher.Block```的方法会用纯Go语言实现,而对于可以并行的加解密模式,则还是会尽量采用AES-NI和SIMD并行处理。您可以通过环境变量```FORCE_SM4BLOCK_AESNI=1```来强制都使用AES-NI实现(和v0.25.0之前版本的行为一样)。请参考[SM4: 单block的性能问题](https://github.com/emmansun/gmsm/discussions/172)。
   250  
   251  **注意**:目前的纯Golang SM4实现(查表实现)是以可变时间运行的!
   252  
   253  ## 与KMS集成
   254  可能您会说,如果我在KMS中创建了一个SM4对称密钥,就不需要本地加解密了,这话很对,不过有种场景会用到:  
   255  * 在KMS中只创建非对称密钥(KEK);
   256  * 对称加解密在本地进行;
   257  * 对称加密密钥,或者称为数据密钥(DEK/CEK),可以在本地通过安全伪随机数函数生成,也可以通过KMS的Data Key API生成(如果有这类API的话),用Data Key API的话,会有DEK/CEK明文传输问题,毕竟KMS需要把DEK/CEK的密文/明文同时返回。
   258  
   259  这种加密方案有什么优点呢?  
   260  * KMS API通常都会限流,譬如200次/秒,通过把对称加解密放在本地进行,可以有效减少KMS交互。
   261  * 减少网络带宽占用。
   262  * 避免明文数据的网络传输。
   263  
   264  当然,前提是用于本地对称加解密的SM4分组密码算法和选用的工作模式性能可以满足需求。