18.1 国密算法概述

国密即国家密码局认定的国产密码算法。主要有SM1,SM2,SM3,SM4。

18.1.1 SM1算法

SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用。

18.1.2 SM2算法

SM2为非对称加密的公钥密码算法,是我国自主设计的椭圆曲线公钥密码算法,该算法基于256位比特(32字节)ECC,安全强度比RSA 2048位高,签名速度与秘钥生成速度都快于RSA。该算法已公开

在实际使用非对称加密时,一般是后端生成密钥对,将公钥交给前端,前端用公钥加密数据,后端用私钥对数据解密。在有些项目中,没有使用HTTPS的时候,登录的口令(用户名/密码),需要进行加密传输的需求,这时候我们就需要采用非对称加密来实现。

18.1.3 SM3算法

SM3为消息摘要算法,可用于数字签名、消息认证、验证数据完整性等,可以用MD5 或 SHA-1 算法作为对比理解,但SM3安全度更高,校验结果为256位比特(32字节)。该算法已公开

18.1.4 SM4算法

SM4是传统的对称加密算法,类似于 DES 或 AES。它的密钥长度和分组长度均为128位,若消息长度过长,需要进行分组,在同一密钥控制下逐组进行加密,若消息长度不足,则要进行填充。

18.2 前端国密算法库

我们在前端选择sm-crypto项目实现国密算法。该项目的地址为https://github.com/JuneAndGreen/sm-crypto

18.2.1 安装

  1. $ tyarn add sm-crypto
  2. $ cnpm install --save sm-crypto

18.2.2 SM2加密解密

本节是使用sm-crypto算法库实现SM2算法加密解密的示例代码。

  • 获取密钥对 ```javascript const sm2 = require(‘sm-crypto’).sm2

let keypair = sm2.generateKeyPairHex()

publicKey = keypair.publicKey // 公钥 privateKey = keypair.privateKey // 私钥

  1. - 加密解密
  2. ```javascript
  3. const sm2 = require('sm-crypto').sm2
  4. const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1
  5. // 加密结果
  6. let encryptData = sm2.doEncrypt(msgString, publicKey, cipherMode)
  7. // 解密结果
  8. let decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode)
  • 签名验签

    ps:理论上来说,只做纯签名是最快的。

  1. const sm2 = require('sm-crypto').sm2
  2. // 纯签名
  3. let sigValueHex = sm2.doSignature(msg, privateKey) // 签名
  4. let verifyResult = sm2.doVerifySignature(msg, sigValueHex, publicKey) // 验签结果
  5. // 纯签名 + 生成椭圆曲线点
  6. let sigValueHex2 = sm2.doSignature(msg, privateKey, {
  7. // 传入事先已生成好的椭圆曲线点,可加快签名速度
  8. pointPool: [sm2.getPoint(), sm2.getPoint(), sm2.getPoint(), sm2.getPoint()],
  9. }) // 签名
  10. let verifyResult2 = sm2.doVerifySignature(msg, sigValueHex2, publicKey) // 验签结果
  11. // 纯签名 + 生成椭圆曲线点 + der编解码
  12. let sigValueHex3 = sm2.doSignature(msg, privateKey, {
  13. der: true,
  14. }) // 签名
  15. let verifyResult3 = sm2.doVerifySignature(msg, sigValueHex3, publicKey, {
  16. der: true,
  17. }) // 验签结果
  18. // 纯签名 + 生成椭圆曲线点 + sm3杂凑
  19. let sigValueHex4 = sm2.doSignature(msg, privateKey, {
  20. hash: true,
  21. }) // 签名
  22. let verifyResult4 = sm2.doVerifySignature(msg, sigValueHex4, publicKey, {
  23. hash: true,
  24. }) // 验签结果
  25. // 纯签名 + 生成椭圆曲线点 + sm3杂凑(不做公钥推导)
  26. let sigValueHex5 = sm2.doSignature(msg, privateKey, {
  27. hash: true,
  28. //传入公钥的话,可以去掉sm3杂凑中推导公钥的过程,
  29. //速度会比纯签名 + 生成椭圆曲线点 + sm3杂凑快
  30. publicKey,
  31. })
  32. let verifyResult5 = sm2.doVerifySignature(msg, sigValueHex5, publicKey, {
  33. hash: true,
  34. publicKey,
  35. })
  36. // 纯签名 + 生成椭圆曲线点 + sm3杂凑 + 不做公钥推 + 添加 userId(长度小于 8192)
  37. // 默认 userId 值为 1234567812345678
  38. let sigValueHex6 = sm2.doSignature(msgString, privateKey, {
  39. hash: true,
  40. publicKey,
  41. userId: 'testUserId',
  42. })
  43. let verifyResult6 = sm2.doVerifySignature(msgString, sigValueHex6, publicKey, {
  44. hash: true,
  45. userId: 'testUserId',
  46. })
  • 获取椭圆曲线点 ```javascript const sm2 = require(‘sm-crypto’).sm2

let poin = sm2.getPoint() // 获取一个椭圆曲线点,可在sm2签名时传入

  1. <a name="o1ai7"></a>
  2. ### 18.2.3 SM3加密
  3. 本节是使用`sm-crypto`算法库实现SM3算法加密的示例代码(摘要算法是单向的)。
  4. ```javascript
  5. const sm3 = require('sm-crypto').sm3
  6. let hashData = sm3('abc') // 杂凑

18.2.4 SM4加密解密

本节是使用sm-crypto算法库实现SM4算法加密解密的示例代码。

  • 加密 ```javascript const sm4 = require(‘sm-crypto’).sm4 // 可以为 utf8 串或字节数组 const msg = ‘hello world! 我是 juneandgreen.’ // 可以为 16 进制串或字节数组,要求为 128 比特 const key = ‘0123456789abcdeffedcba9876543210’

// 加密,默认输出 16 进制字符串,默认使用 pkcs#5 填充 let encryptData = sm4.encrypt(msg, key) // 加密,不使用 padding let encryptData = sm4.encrypt(msg, key, {padding: ‘none’}) // 加密,不使用 padding,输出为字节数组 let encryptData = sm4.encrypt(msg, key, {padding: ‘none’, output: ‘array’})

  1. - 解密
  2. ```javascript
  3. const sm4 = require('sm-crypto').sm4
  4. // 可以为 16 进制串或字节数组
  5. const encryptData = '0e395deb10f6e8a17e17823e1fd9bd98a1bff1df508b5b8a1efb79ec633d1bb129432ac1b74972dbe97bab04f024e89c'
  6. // 可以为 16 进制串或字节数组,要求为 128 比特
  7. const key = '0123456789abcdeffedcba9876543210'
  8. // 解密,默认输出 utf8 字符串,默认使用 pkcs#5 填充
  9. let decryptData = sm4.decrypt(encryptData, key)
  10. // 解密,不使用 padding
  11. let decryptData = sm4.decrypt(encryptData, key, {padding: 'none'})
  12. // 解密,不使用 padding,输出为字节数组
  13. let decryptData = sm4.decrypt(encryptData, key, {padding: 'none', output: 'array'})

18.3 在前端应用国密算法的实例

18.3.1 配置路由

config/routes.ts

  1. {
  2. name: '加密解密',
  3. icon: 'lock',
  4. path: '/crypto',
  5. component: './Crypto',
  6. },

18.3.2 前端代码

建立新的目录和文件src/pages/Crypto/index.tsx

  1. import React from 'react';
  2. import { PageContainer } from '@ant-design/pro-layout';
  3. import { request } from 'umi';
  4. import { Form, notification } from 'antd';
  5. import ProCard from '@ant-design/pro-card';
  6. import ProForm, {
  7. ProFormProps,
  8. ProFormText,
  9. ProFormRadio,
  10. ProFormTextArea,
  11. } from '@ant-design/pro-form';
  12. //之前用sm2.generateKeyPairHex()计算密钥对后的公钥
  13. const publicKey = "04ac0d181497f663c38ecc246183085a272f28c2842aa739fe087cd607d9b32d3dc3f897f45baf4c3196e670437e4af00b935cd263370f028563ff830229007a18"
  14. //可以为 16 进制串或字节数组,要求为 128 比特
  15. const key = '0123456789abcdeffedcba9876543210'
  16. async function sendEncryptData(algorithm:number, encryptData:string) {
  17. try {
  18. const result = await request('/api/decryptData', {
  19. method: 'POST',
  20. data: {
  21. algorithm,
  22. encryptData,
  23. }
  24. })
  25. if(result.success) {
  26. const options = {
  27. message: '解密结果',
  28. description: result.decryptData,
  29. }
  30. if(result.decryptSuccess)
  31. notification.info(options)
  32. else
  33. notification.error(options)
  34. }
  35. } catch(e) {
  36. }
  37. }
  38. const cryptoPage: React.FC = () => {
  39. const [form] = Form.useForm();
  40. const FormProps: ProFormProps = {
  41. form,
  42. layout: "horizontal",
  43. initialValues: {
  44. algorithm: 3,
  45. message: 'password',
  46. },
  47. submitter: {
  48. searchConfig: {
  49. submitText: '加密后发送',
  50. },
  51. },
  52. onValuesChange: (changedValus) => {
  53. if(changedValus.algorithm === 3)
  54. form.setFieldsValue({
  55. message: 'password'
  56. })
  57. else if(changedValus.algorithm != undefined)
  58. form.setFieldsValue({
  59. message: ''
  60. })
  61. },
  62. onFinish: async (formData: any) => {
  63. let encryptData = ''
  64. const { algorithm, message } = formData
  65. switch(algorithm) {
  66. case 2:
  67. //非对称加密
  68. const sm2 = require('sm-crypto').sm2
  69. const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1
  70. encryptData = sm2.doEncrypt(message, publicKey, cipherMode)
  71. break;
  72. case 4:
  73. //对称加密
  74. const sm4 = require('sm-crypto').sm4
  75. //加密,默认输出 16 进制字符串,默认使用 pkcs#5 填充
  76. encryptData = sm4.encrypt(message, key)
  77. break;
  78. default:
  79. // 杂凑
  80. const sm3 = require('sm-crypto').sm3
  81. encryptData = sm3(message)
  82. }
  83. form.setFieldsValue({
  84. encryptResult: encryptData
  85. })
  86. sendEncryptData(algorithm, encryptData)
  87. },
  88. }
  89. return (
  90. <PageContainer header={{breadcrumb: {},}} title="国密加密解密">
  91. <ProCard>
  92. <ProForm {...FormProps}>
  93. <ProFormRadio.Group name="algorithm"
  94. label="算法类型"
  95. options={[
  96. { value: 2, label: 'SM2 非对称加密' },
  97. { value: 3, label: 'SM3 摘要算法' },
  98. { value: 4, label: 'SM4 对称加密' },
  99. ]} />
  100. <ProFormText name="message"
  101. label="明文内容"
  102. fieldProps ={{ autoComplete:"off" }}/>
  103. <ProFormTextArea name="encryptResult"
  104. label="加密结果" disabled={true} />
  105. </ProForm>
  106. </ProCard>
  107. </PageContainer>
  108. )
  109. }
  110. export default cryptoPage;

image.png

18.3.3 配套的Mock代码

mock/crypto.ts

  1. import { Request, Response } from 'express';
  2. function decryptData(req: Request, res: Response, u: string) {
  3. const key = '0123456789abcdeffedcba9876543210'
  4. //const keypair = sm2.generateKeyPairHex()
  5. const privateKey = "72b7015c42fc937d6a7197a5595c13ed683e87d32ea6180514740d6263b3c88d"
  6. const { algorithm=3, encryptData=''} = {...req.body}
  7. const result = {
  8. success: true,
  9. decryptSuccess: true,
  10. decryptData: '',
  11. }
  12. switch(algorithm) {
  13. case 2:
  14. const sm2 = require('sm-crypto').sm2
  15. const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1
  16. result.decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode)
  17. break;
  18. case 4:
  19. const sm4 = require('sm-crypto').sm4
  20. result.decryptData = sm4.decrypt(encryptData, key)
  21. break;
  22. default:
  23. const sm3 = require('sm-crypto').sm3
  24. if(encryptData == sm3('password'))
  25. result.decryptData = '哈希计算结果一致'
  26. else {
  27. result.decryptSuccess = false
  28. result.decryptData = '哈希计算结果不一致'
  29. }
  30. }
  31. return res.json(result);
  32. }
  33. export default {
  34. 'POST /api/decryptData': decryptData,
  35. };

18.4 使用SM2和SM3保护用户密码

18.4.1 处理逻辑

我们可以组合使用SM2和SM3保护用户密码,以实现不在后端保存明文密码不通过网络传递明文密码的两个目标:

  1. 后端保存用户密码经SM3算法计算后的哈希值
  2. 前端使用SM2算法的公钥加密用户密码后传输
  3. 后端使用SM2算法的私钥解密收到的用户密码
  4. 后端使用SM3算法计算用户密码的哈希值
  5. 后端判断新计算的哈希值与之前保存的结果是否一致

    18.4.2 在前端加密用户密码

    pages/user/Login/index.tsx中修改登录部分的代码 ```diff
  • const loginResult = await login({ …values, type: loginType });
  • const publicKey = “04ac0d181497f663c38ecc246183085a272f28c2842aa739fe087cd607d9b32d3dc3f897f45baf4c3196e670437e4af00b935cd263370f028563ff830229007a18”
  • let { password, …rest } = values +
  • if(password != undefined) {
  • const sm2 = require(‘sm-crypto’).sm2
  • const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1
  • password = sm2.doEncrypt(password, publicKey, cipherMode)
  • }
  • const loginResult = await login({ …rest, password, type: loginType }); ```

    18.4.3 Mock中解密和计算哈希

    mock/user.ts中修改登录响应函数(请自行仔细比较代码的变化)
  1. const privateKey = "72b7015c42fc937d6a7197a5595c13ed683e87d32ea6180514740d6263b3c88d"
  2. //字符串admin经过SM3进行哈希计算后的结果
  3. const adminHash = "dc1fd00e3eeeb940ff46f457bf97d66ba7fcc36e0b20802383de142860e76ae6"
  4. //字符串user经过SM3进行哈希计算后的结果
  5. const userHash = "92e7fbdcca8b9f36be0638e48e77cbeeb49ef15886b6cd12d46e09d74a232a81"
  6. async function login(req: Request, res: Response) {
  7. let { password, username, captcha, type } = req.body;
  8. const result:TYPE.QueryResult & {type:string} = {
  9. success: true,
  10. type,
  11. data: {}
  12. }
  13. console.log("body = ", req.body)
  14. await waitTime(2000);
  15. if(type === 'account') {
  16. if(password != undefined ) {
  17. //解密获得加密前的明文密码
  18. const sm2 = require('sm-crypto').sm2
  19. const cipherMode = 1 // 1 - C1C3C2,0 - C1C2C3,默认为1
  20. password = sm2.doDecrypt(password, privateKey, cipherMode) // 解密结果
  21. //计算前端密码的哈希值
  22. const sm3 = require('sm-crypto').sm3
  23. password = sm3(password)
  24. }
  25. //将前端密码的哈希值与后端保存的对比
  26. if (username === 'admin' && password === adminHash ) {
  27. result.data['token'] = makeToken(username)
  28. }
  29. if (username === 'user' && password === userHash ) {
  30. result.data['token'] = makeToken(username)
  31. }
  32. } else {
  33. if(currentCaptcha != captcha) {
  34. result.success = false;
  35. result.errorMessage = '验证码错误'
  36. } else
  37. result.data['token'] = makeToken()
  38. }
  39. return res.json(result);
  40. }

18.5 参考信息

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。