【常规】前端常见的加密方式

前端加密常规操作

不错鼓励并赞赏 标签: HTML CSS Javascript nodeJs      评论 / 2023-08-29

近段时间又遇到了前端加密的相关知识点,还是那样,在这里mark一下。

前端加密主要是指在浏览器端执行的加密过程,用于增强数据的隐私和安全性。这样的加密操作一般用于在数据从客户端发送到服务器之前保护数据。需要注意的是,依赖纯前端加密可能会有安全隐患,因为经验丰富的攻击者可以直接修改或绕过前端代码。

老规矩,上干货

  • 哈希函数:

  • MD5: 曾经被广泛用于计算数据的哈希值,如用户密码的哈希,但现在因安全问题不建议用于敏感数据。
    SHA-256: 在一些安全要求更高的应用中计算数据的哈希值,如加密货币。
    SHA-*:更多SHA相关加密可以参考google链接:https://code.google.com/archive/p/crypto-js/
import CryptoJS from 'crypto-js';
// MD5案例:
var hash = CryptoJS.MD5("Message");

// SHA-256
var hash = CryptoJS.SHA256("Message");

  • 文本加密库:

  • CryptoJS: 一个流行的JavaScript库,提供了诸如AES、DES、Rabbit等多种加密算法的实现。
    案例:用户在前端输入数据后,使用CryptoJS进行AES加密,然后再发送到后台。
// AES加密:
import React, { useState } from 'react';
import CryptoJS from 'crypto-js';

function AesDemo() {
   const secretKey = 'my-secret-key'; // 请确保这个密钥在实际应用中是安全的
   const [text, setText] = useState('');
   const [encryptedText, setEncryptedText] = useState('');
   const [decryptedText, setDecryptedText] = useState('');

   // 加密
   const encrypted = CryptoJS.AES.encrypt(text, secretKey).toString();
   setEncryptedText(encrypted);
   // 解密
   const decrypted = CryptoJS.AES.decrypt(encryptedText,secretKey).toString(CryptoJS.enc.Utf8);
   setDecryptedText(decrypted);


    return (
        ...
    )
}
  • Web Cryptography API:

    这是现代浏览器内置的加密API,支持摘要、加密、解密、签名和校验等操作。
    案例:在Web应用中,当用户提交表单前,使用Web Cryptography API对数据进行RSA加密。


特性如下:

生成密钥:可以生成用于加密、签名或其他目的的密钥对或秘密密钥。
加密和解密:支持多种常用的加密和解密操作。
签名和验证:可以为数据创建数字签名,并验证签名是否有效。
哈希:提供基本的哈希函数,如 SHA-256。
随机数生成:生成加密安全的随机数。
导入和导出密钥:可以在不同的格式之间导入和导出密钥。

// 定义一个异步函数来演示加密和解密过程
async function encryptDecryptDemo() {
  // 获取浏览器的 subtle 加密接口
  const subtle = window.crypto.subtle;

  // 将要加密的字符串和密钥密码转换为 Uint8Array
  const rawData = new TextEncoder().encode("Hello, World!");
  const passphrase = new TextEncoder().encode("secret-key");

  // 使用提供的密码创建一个基于 PBKDF2 的密钥
  const passwordKey = await subtle.importKey("raw", passphrase, { name: "PBKDF2" }, false, ["deriveKey"]);
  
  // 生成随机的 salt 和 iv (初始化向量) 用于加密过程
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const iv = crypto.getRandomValues(new Uint8Array(12));

  // 使用 PBKDF2 和上面生成的 salt 从密码密钥派生出一个 AES 密钥
  const aesKey = await subtle.deriveKey(
    { name: "PBKDF2", salt: salt, iterations: 1000, hash: "SHA-256" },
    passwordKey,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"]
  );

  // 使用派生出的 AES 密钥和初始化向量 (iv) 对原始数据进行加密
  const encryptedData = await subtle.encrypt({ name: "AES-GCM", iv: iv }, aesKey, rawData);
  
  // 使用同一个 AES 密钥和初始化向量 (iv) 对加密后的数据进行解密
  const decryptedData = await subtle.decrypt({ name: "AES-GCM", iv: iv }, aesKey, encryptedData);
  
  // 将解密后的 Uint8Array 转回字符串并输出
  console.log(new TextDecoder().decode(decryptedData)); // 输出: "Hello, World!"
}

// 调用上述函数进行加密解密的演示
encryptDecryptDemo();

  • 随机数生成:

  • 在前端生成安全的随机数或字符串,用于加盐或其他随机化处理。
    案例:生成一个随机的"盐"来与用户密码合并,然后进行哈希。
// 使用 crypto-js 库,所以先安装它
// npm install crypto-js

import CryptoJS from 'crypto-js';

// 生成随机盐
function generateSalt(length) {
  const array = new Uint8Array(length);
  window.crypto.getRandomValues(array);

  return Array.from(array).map(byte => byte.toString(16).padStart(2, '0')).join('');
}

// 对密码进行哈希
function hashPassword(password, salt) {
  const saltedPassword = password + salt;
  return CryptoJS.SHA256(saltedPassword).toString();
}

// 使用
const userPassword = 'user123'; // 从用户输入获取

const salt = generateSalt(16); // 生成 16 字节的随机盐
const hashedPassword = hashPassword(userPassword, salt);

console.log(`Salt: ${salt}`);
console.log(`Hashed Password: ${hashedPassword}`);

存储盐和哈希密码时,必须确保它们都在数据库中安全地存储。当用户试图登录时,需要从数据库中取出相应的盐,再次哈希用户输入的密码,然后与存储的哈希值进行比较。如果两个哈希值匹配,那么密码正确,用户可以登录。
这种加盐和哈希的方法可以有效地防止彩虹表攻击,并增加暴力攻击的难度。

  • 密码派生函数 (如PBKDF2):
    这些函数用于从用户提供的密码生成长且随机的密钥。
    案例:用户选择一个密码,前端使用PBKDF2生成一个密钥,然后使用该密钥加密数据。

  • 安全通信协议:
    WebSocket Secure (WSS): 安全的WebSocket通信。
    案例:在聊天应用中,使用WSS确保用户之间的聊天内容在传输时是加密的。

  • 隐私增强技术:
    zk-SNARKs: 前端实现的零知识证明,用于在不泄露数据的情况下证明某些信息。
    案例:在某些基于Web的加密货币钱包中,使用zk-SNARKs技术进行隐私交易。
    场景描述:Alice 想要在一个基于 Web 的加密货币钱包中向 Bob 发送一笔加密货币。但她不希望其他人知道交易的金额或她是交易的参与者。为了实现这一点,她决定使用支持 zk-SNARKs 的加密货币钱包进行隐私交易。
    操作步骤:
    a. 生成证明:
    Alice 在她的浏览器钱包中选择要发送的金额和收款人(Bob)。她的钱包使用 zk-SNARKs 为这笔交易生成一个零知识证明。这个证明可以验证交易是有效的,但不会泄露交易的细节(如金额、发送者或接收者)。
    b. 提交交易:
    Alice 的钱包将零知识证明与其他必要的交易数据(但不包括明文的敏感细节)一起提交到网络。
    c. 验证证明:
    网络上的节点(或称为矿工)接收到这笔交易,使用 zk-SNARKs 验证提供的证明确保它是有效的,而不需要知道具体的交易细节。
    d. 完成交易:
    一旦证明被验证为真实且有效,这笔交易将被添加到区块链上,Bob 的钱包余额增加,而 Alice 的钱包余额相应减少。
    代码和技术细节:
    虽然实际的 zk-SNARKs 实现和整合可能涉及深入的数学和编程技术,但在高级别上,前端应用可能会调用一些库或 API,例如 snarkjs 或其他 zk-SNARKs 工具套件,以帮助生成和验证证明。注意:这里仅为概念演示,真正的实现会更为复杂:

import { generateProof, verifyProof } from 'some-zk-snarks-library';

const transactionDetails = {
  amount: 10,
  sender: 'Alice',
  recipient: 'Bob'
};

// 在 Alice 的前端钱包中
const proof = generateProof(transactionDetails);

// 发送 proof 到网络,而不是真实的 transactionDetails

// 在网络的节点上
const isValid = verifyProof(proof);
if (isValid) {
  // 将交易添加到区块链
}

  • 开源库或工具:

  • OpenPGP.js: 提供PGP加密和解密的JavaScript库。
    案例:Web邮件客户端使用OpenPGP.js为邮件提供端到端加密。
// 引入 openpgp 库
const openpgp = require('openpgp');

(async () => {
    // 将公钥和私钥放在反引号 (``) 内,以避免因空格或制表符造成的错误
    const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`;

    const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----`; // 加密的私钥
    const passphrase = `yourPassphrase`; // 私钥的加密密码

    // 读取并解析公钥
    const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });

    // 读取、解析并解密私钥
    const privateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
        passphrase
    });

    // 使用公钥加密消息,并使用私钥签名(可选)
    const encrypted = await openpgp.encrypt({
        message: await openpgp.createMessage({ text: 'Hello, World!' }), // 作为 Message 对象的输入
        encryptionKeys: publicKey,
        signingKeys: privateKey // 可选的签名
    });
    console.log(encrypted); // 打印加密的消息

    // 读取并解析加密的消息
    const message = await openpgp.readMessage({
        armoredMessage: encrypted
    });

    // 使用私钥解密消息,并使用公钥验证签名(如果有)
    const { data: decrypted, signatures } = await openpgp.decrypt({
        message,
        verificationKeys: publicKey,
        decryptionKeys: privateKey
    });
    console.log(decrypted); // 打印解密后的消息

    // 验证签名的有效性(仅限已签名的消息)
    try {
        await signatures[0].verified; // 如果签名无效则抛出异常
        console.log('Signature is valid');
    } catch (e) {
        throw new Error('Signature could not be verified: ' + e.message);
    }
})();

以上代码摘选Github,并添加了注释:https://github.com/openpgpjs/openpgpjs,确保安全地存储和管理私钥。在上面的示例中,私钥和密码是硬编码的,但在实际应用中,你可能需要更安全的方式来存储和获取它们,例如使用密码管理器或硬件密钥存储。
这只是一个简化的示例。在真实的环境中,你可能还需要处理错误、密钥管理、用户交互等多种复杂情况。
为了最大化安全性,建议经常更新并使用最新版本的 OpenPGP.js 和其他相关库。

  • LocalStorage加密工具:

  • SecureLS: 一个用于加密LocalStorage数据的库。
    案例:Web应用中的敏感用户设置或数据在存储到LocalStorage前使用SecureLS加密。
// 引入 SecureLS 库
const SecureLS = require('secure-ls');
// 初始化 SecureLS 实例
const ls = new SecureLS({ encodingType: 'aes', isCompression: true, encryptionSecret: 'your-secret-key' });

// 使用 SecureLS 存储加密后的数据到 LocalStorage
function saveEncryptedData(key, data) {
    ls.set(key, data);
}

// 从 LocalStorage 中获取数据,并自动进行解密
function getDecryptedData(key) {
    return ls.get(key);
}

// 示例使用
const sensitiveData = {
    userSettings: {
        theme: 'dark',
        notifications: true
    }
};

// 保存加密数据
saveEncryptedData('userSettings', sensitiveData);

// 从 LocalStorage 获取并解密数据
const decryptedData = getDecryptedData('userSettings');
console.log(decryptedData); // 输出: { userSettings: { theme: 'dark', notifications: true } }

这里的 your-secret-key 应该是一个复杂且难以猜测的字符串。在生产环境中,确保此密钥的安全性。
虽然 SecureLS 增加了对 LocalStorage 数据的安全性,但它仍然不是长期存储敏感数据的推荐方法。对于非常敏感的数据,最好不要存储在客户端,或者使用更安全的持久性解决方案。

  • Client-Side Field Level Encryption:

  • 在客户端对特定字段进行加密,然后再将其发送到数据库。
    案例:在Web应用中,用户的某些敏感字段(如医疗记录)在存储到数据库之前在客户端进行加密。

加密并存储数据:

const CryptoJS = require('crypto-js');

const SECRET_KEY = 'your-secret-key';

function encryptSensitiveData(data) {
    // 每次加密都生成一个新的随机 IV
    const iv = CryptoJS.lib.WordArray.random(128 / 8);

    const cipherText = CryptoJS.AES.encrypt(JSON.stringify(data), SECRET_KEY, { iv: iv }).toString();

    // 返回加密的数据和 IV
    return {
        cipherText,
        iv: iv.toString(CryptoJS.enc.Hex)  // 将 IV 转换为字符串以便存储或传输
    };
}

const sensitiveData = {
    medicalInfo: "Sensitive medical information here"
};

const encryptedDataWithIV = encryptSensitiveData(sensitiveData);

// 将加密的数据和 IV 一起存储
yourDatabaseAPI.saveUserData(encryptedDataWithIV);

从数据库读取并解密数据:

function decryptSensitiveData(cipherText, iv) {
    // 将 IV 从字符串转换回 WordArray 格式
    const ivWordArray = CryptoJS.enc.Hex.parse(iv);
    const bytes = CryptoJS.AES.decrypt(cipherText, SECRET_KEY, { iv: ivWordArray });
    return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
}

// 从数据库获取加密的数据和 IV
const fetchedData = yourDatabaseAPI.getUserData();

const decryptedData = decryptSensitiveData(fetchedData.cipherText, fetchedData.iv);
console.log(decryptedData);

我们将 IV 与加密的数据一起存储,但是将它们分开。当我们需要解密数据时,我们使用存储的 IV 和 SECRET_KEY 一起进行解密。

那会有小伙伴会问IV到底有啥用,笔者简单总结了一下:就是混淆明文内容,增加了明文的长度,让明文加密后的密文也是具有随机性 ,具体区别如下几点:

  1. 防止密文被直接攻击:IV扰乱了明文数据,使得即使相同的明文,加密出来的密文也不相同。
  2. 防止密钥被推断:IV与密钥无关,使得通过密文推断密钥的攻击变得困难。
  3. 支持密文认证:IV可以被用来对密文进行认证。

  • SM加密:

"SM" 系列的加密算法是指中国国家标准的加密算法。在近些年,中国国家密码管理局发布了一系列的国产密码标准,被称为 "SM" (商密,意为“商用密码”)系列。以下是一些核心的 SM 系列加密方式:

  1. SM1:
    用途: 对称加密。
    详情: SM1 是一个对称加密算法。它在中国内部的许多系统中使用,但其算法细节并未公开。所以只有做项目的时候,交付合同中有要求,伴随需求文档的说明即可使用。暂时无案例代码。

  2. SM2:
    用途: 非对称加密、签名、密钥交换。
    详情: SM2 是基于椭圆曲线密码(ECC)的一个算法。与 RSA 和其他 ECC 算法相比,SM2 在某些场景中可能提供更高的效率。它包含加密、签名、密钥交换三个部分。

const { SM2 } = require('gm-crypto')

const { publicKey, privateKey } = SM2.generateKeyPair()
const originalData = 'SM2 椭圆曲线公钥密码算法'

const encryptedData = SM2.encrypt(originalData, publicKey, {
  inputEncoding: 'utf8',
  outputEncoding: 'base64'
})

const decryptedData = SM2.decrypt(encryptedData, privateKey, {
  inputEncoding: 'base64',
  outputEncoding: 'utf8'
})
  1. SM3:
    用途: 哈希函数。
    详情: SM3 是一个密码哈希函数,输出长度为 256 位。它的安全性和 SHA-256 相当,但是是独立设计的。
const { SM3 } = require('gm-crypto')

console.log(SM3.digest('abc'))
console.log(SM3.digest('YWJj', 'base64'))
console.log(SM3.digest('616263', 'hex', 'base64'))
  1. SM4:
    用途: 对称加密。
    详情: SM4,也被称为 SMS4,是一个 128 位的对称加密算法。它曾被用于中国的无线局域网标准 WAPI 中,现在也被用于其他应用。
const { SM4 } = require('gm-crypto')

const key = '0123456789abcdeffedcba9876543210' // Any string of 32 hexadecimal digits
const originalData = 'SM4 国标对称加密'

/**
 * Block cipher modes:
 * - ECB: electronic codebook
 * - CBC: cipher block chaining
 */

let encryptedData, decryptedData

// ECB
encryptedData = SM4.encrypt(originalData, key, {
  inputEncoding: 'utf8',
  outputEncoding: 'base64'
})
decryptedData = SM4.decrypt(encryptedData, key, {
  inputEncoding: 'base64',
  outputEncoding: 'utf8'
})

// CBC
const iv = '0123456789abcdeffedcba9876543210' // Initialization vector(any string of 32 hexadecimal digits)
encryptedData = SM4.encrypt(originalData, key, {
  iv: iv,
  mode: SM4.constants.CBC,
  inputEncoding: 'utf8',
  outputEncoding: 'hex'
})
decryptedData = SM4.decrypt(encryptedData, key, {
  iv: iv,
  mode: SM4.constants.CBC,
  inputEncoding: 'hex',
  outputEncoding: 'utf8'
})
  1. SM9:
    用途: 基于身份的非对称加密和签名。
    详情: SM9 是一个基于身份的密码体系,允许基于用户的身份(如电子邮件地址或用户名)进行加密和签名,而无需先获取此身份的公钥。

关于SM9的库可以参考java库:https://github.com/guanzhi/GmSSL的文档,使用https://github.com/guanzhi/GmSSL-JS的库。具体请参考项目交付的需求文档。

这些算法被设计为与国际标准相竞争,同时满足中国内部的安全和政策要求。在中国,许多政府和大型企业的项目都被要求或推荐使用 SM 系列的算法,尤其是在关键基础设施和国家安全相关的场景中。

参考文献:

https://github.com/sytelus/CryptoJS
https://code.google.com/archive/p/crypto-js/
https://github.com/openpgpjs/openpgpjs
https://github.com/guanzhi/GmSSL

    Hi 看这里!

    大家好,我是PRO

    我会陆续分享生活中的点点滴滴,当然不局限于技术。希望笔墨之中产生共鸣,每篇文章下面可以留言互动讨论。Tks bd!

    博客分类

    您可能感兴趣

    作者推荐

    呃,突然想说点啥

    前端·博客

    您的鼓励是我前进的动力---

    使用微信扫描二维码完成支付