Crypto加密你的明文密码

前言

密码不能明文传输,所以我们要加密。
以前可能我们需要借助诸如 cryptoJs一样的三方库来做,可现在浏览器原生支持了window.crypto

浏览器

crypto-helper.ts

type Cfg = {
  // 算法参数,定义了生成 RSA 密钥对时使用的具体算法和相关参数,通常是一个 RsaHashedKeyGenParams 类型的对象。是生成 RSA 密钥对时的关键配置项,它包含以下字段:
  // name:指定要使用的算法名称,比如RSA-OAEP(推荐)、RSA-PSS、RSA-SHA1、RSA-SHA256、RSA-SHA384、RSA-SHA512
  // modulusLength:指定 RSA 密钥的模数长度,通常为 2048(推荐)、3072 或 4096 位
  // publicExponent:指定 RSA 公钥的公共指数,通常为 65537,0x10001(推荐 也就是new Uint8Array([1, 0, 1]))
  // hash: 指定用于 OAEP 填充的哈希函数,如 SHA-256(推荐)、SHA-384 或 SHA-512
  algorithm: RsaHashedKeyGenParams;

  // 指定生成的密钥是否可以被导出
  // - true: 密钥可以被导出为原始格式(如 CryptoKey 对象)
  // - false: 密钥不能被导出,只能在当前上下文中使用
  // 建议在安全敏感的场景中设置为 false,以防止密钥被泄露
  extractable: boolean;

  // 指定生成的密钥可以被使用的用途,常见的用途包括:
  // - 'encrypt': 密钥可用于加密数据
  // - 'decrypt': 密钥可用于解密数据
  // - 'sign': 密钥可用于生成数字签名
  // - 'verify': 密钥可用于验证数字签名
  // - 'wrapKey': 密钥可用于包装其他密钥
  // - 'unwrapKey': 密钥可用于解包其他密钥
  // - 'deriveKey': 密钥可用于派生其他密钥
  // - 'deriveBits': 密钥可用于派生位
  keyUsages: ReadonlyArray<KeyUsage>;
};

// 生成 RSA 密钥对
export async function generateKeyPair() {
  const cfg: Cfg = {
    algorithm: {
      name: 'RSA-OAEP',
      modulusLength: 2048,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: 'SHA-256',
    },
    extractable: true,
    keyUsages: ['encrypt', 'decrypt'],
  };
  const keyPair = await window.crypto.subtle.generateKey(cfg.algorithm, cfg.extractable, cfg.keyUsages);
  return keyPair;
}

// 使用公钥加密
export async function encrypt(data, publicKey) {
  const encoder = new TextEncoder();
  const dataBuffer = encoder.encode(data);
  const encrypted = await window.crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, dataBuffer);
  return encrypted;
}

// 使用私钥解密
export async function decrypt(encryptedData, privateKey) {
  const decrypted = await window.crypto.subtle.decrypt({ name: 'RSA-OAEP' }, privateKey, encryptedData);
  const decoder = new TextDecoder();
  return decoder.decode(decrypted);
}

使用

import { generateKeyPair, encrypt, decrypt } from '$/utils/crypto-helper';

const testCrypto = async ()=>{
  const keyPair = await generateKeyPair();
  console.log('获得密钥对', keyPair);
  const encryptedData = await encrypt('hello2025', keyPair.publicKey);
  console.log('加密后的数据', encryptedData);
  const decryptedData = await decrypt(encryptedData, keyPair.privateKey);
  console.log('解密后的数据', decryptedData);
}
testCrypto()

400

服务端(node)

crypto-helper.ts

import crypto from 'crypto';

// 生成 RSA 密钥对
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
});

// 主要用这个易于通信和存储的的导出的公钥密钥字符串来进行操作(即上一步生成的 KeyPair 最好导出,便于持久存储和传输)
// type: 'spki' - 这个类型只适用于公钥
// type: 'pkcs8' - 这是私钥的标准导出格式
const publicKeyStr = keyPair.publicKey.export({ type: 'spki', format: 'pem' }).toString();
const privateKeyStr = keyPair.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();

// 使用公钥加密
const encrypt = (data, publicKey) => {
  const buffer = Buffer.from(data, 'utf8');
  const encrypted = crypto.publicEncrypt(publicKey, buffer);
  return encrypted
};

// 使用私钥解密
const decrypt = (encryptedData, privateKey) => {
  const buffer = Buffer.from(encryptedData, 'base64');
  const decrypted = crypto.privateDecrypt(privateKey, buffer);
  return decrypted;
};

// 示例
const data = 'Hello, World!';
const encryptedData = encrypt(data, publicKey);
console.log('加密后端的数据:', encryptedData);

const decryptedData = decrypt(encryptedData, privateKey);
console.log('解密后的数据:', decryptedData.toString());

600

前后端结合

一般业务场景是生成密钥后(运维、前端谁生成都无所谓),

然后前端拿到公钥 来加密数据

// 导入本地PEM文件内容
import publicKeyPem from './publicKey.pem?raw';

// 从PEM内容(字符串)生成RSA公钥(CryptoKey格式)
async function importPublicKeyFromPem(pemText: string): Promise<CryptoKey> {

  // 移除PEM头尾和换行符
  const pemHeader = "-----BEGIN PUBLIC KEY-----";
  const pemFooter = "-----END PUBLIC KEY-----";
  const pemContents = pemText.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");

  // Base64解码
  const binaryDer = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0));

  // 导入公钥
  return await window.crypto.subtle.importKey(
    "spki",
    binaryDer,
    {
      name: "RSA-OAEP",
      hash: "SHA-256"
    },
    false,
    ["encrypt"]
  );
}

// 使用公钥加密
export async function encrypt(data: string): Promise<string> {
  const publicKey = await importPublicKeyFromPem(publicKeyPem);
  const encoder = new TextEncoder();
  const dataBuffer = encoder.encode(data);
  const encrypted = await window.crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, dataBuffer);

  // 将ArrayBuffer转换为Base64字符串
  const encryptedArray = new Uint8Array(encrypted);
  const base64String = btoa(String.fromCharCode(...encryptedArray));
  console.log('base64String', base64String);
  
  return base64String;
}

后端拿到私钥后去解密

import fs from 'fs-extra';
import crypto from 'crypto';
import type { KeyPairKeyObjectResult } from 'crypto';


let privateKey = fs.readFileSync('key-pair/privateKey.pem', 'utf8');

// 生成密钥对,并写入到文件中(记得不要重复生成,否则无法处理旧数据了)
export const genKeyPair = (needExport = false) => {
  const keyPair = crypto.generateKeyPairSync('rsa', {
    modulusLength: 2048,
  });
  const result: KeyPairKeyObjectResult & { publicKeyStr?: string; privateKeyStr?: string } = { ...keyPair };
  if (needExport) {
    const publicKeyStr = keyPair.publicKey.export({ type: 'spki', format: 'pem' }).toString();
    const privateKeyStr = keyPair.privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
    result.publicKeyStr = publicKeyStr;
    result.privateKeyStr = privateKeyStr;
    fs.outputFileSync('key-pair/publicKey.pem', publicKeyStr);
    fs.outputFileSync('key-pair/privateKey.pem', privateKeyStr);
  }
  return result;
};

// 使用私钥解密
export const decrypt = (encryptedData) => {
  // 将PEM格式的私钥字符串转换为KeyObject
  privateKey = crypto.createPrivateKey(privateKey);
  // 开始解密 - 指定与前端相同的填充方式和哈希算法
  const buffer = Buffer.from(encryptedData, 'base64');
  const decrypted = crypto.privateDecrypt({
    key: privateKey,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
    oaepHash: 'sha256'  // 与前端的SHA-256保持一致
  }, buffer);
  return decrypted.toString('utf8');
};

const newPwd = "ElYGNcNwTv6FpFmvFqI28nweJID9q3Lhv1RYJj4l2F1W/FfTqE3OKS24iNSybnfBXi+pS41jBpVrcUE/KyTku4GWJbca/eHJlxB88uaSyiEb9ZX+VjJzNC20O3K2QI/TkvysLm/0TwrhblvMGmEAxlaw80KT4Vc5ZeaTn5qBx8d8C9rJgCn2zoCtsuso0k16tXjllpBp8RehGBWq8XrDnGpSgz1u0Y5KhTgYKniG4ruTgfj3dhcNrzAAJUpC1cbBBMEWBgEC2ZWP4QJf7lyNvthwqoYkfIadU8z38cVPyHXGqhvq9jdIasfcbrowrTAgFYh4BZMSdz6S7v2V/0PsLQ=="
const res = decrypt(newPwd);
console.log(res);

严格来说,其实大部分场景 连后端也不需要去解密,
比如密码,前后端直接就拿加密后的数据传输和对比以及入库就行,
这样也能杜绝后端哪些心怀不轨的人员 干些见不得人的勾当!

我刚才又想到了,后端即便不需要密钥,他也能直接拿加密后的数据登录进去,所以是防不住后端的。只不过不让他知道明文密码,为了防止万一你在别的系统设置了相同的密码,就给他有乘之机了,特别是当用户在多个系统中使用相同的密码时,这种保护尤为重要。

如果数据库被泄露,攻击者只能获取到加密后的数据,而无法直接使用这些数据进行登录。这增加了攻击者获取有效登录凭证的难度,从而提高了系统的安全性。

总结

1. 密钥对的生成和存储

  • 生成时机:密钥对(公钥和私钥)应该在项目启动时或部署时生成(最后是脱离项目单独生成),并且应该持久化存储,以确保在整个项目生命周期内保持一致。重新生成密钥对会导致之前加密的数据无法解密。
  • 存储位置:密钥对可以存储在数据库或服务器文件系统中,具体选择取决于安全需求和应用场景。
    • 数据库存储:便于管理和访问控制,适合需要集中管理密钥的场景,不过需要使用 text 字段 vachar 长度不够。
    • 文件系统存储:简单直接,适合不需要复杂管理的场景。

2. 公钥的分发

  • 前端嵌入:将公钥提前写死到前端项目中是完全可行的。公钥是设计用来公开分发的,因此嵌入到前端项目中不会影响安全性。这种方法可以减少网络请求,提高性能。
  • 动态获取:如果需要更高的灵活性,也可以在前端启动时从后端动态获取公钥。这种方法适用于公钥可能需要更新的场景。

3. 私钥的保护

  • 私钥的用途:私钥用于解密数据和生成签名,必须严格保密。私钥泄露会导致数据被解密和签名被伪造,严重威胁系统的安全性。
  • 存储和访问控制:私钥应该存储在安全的环境中,例如服务器的文件系统或数据库中,并且只有授权的应用程序和用户可以访问。

4. 前端是否需要公钥

公钥用来加密,私钥用来解密!

  • 需要公钥:如果前端需要对数据进行加密,然后将加密后的数据发送到后端,那么前端需要公钥。公钥可以嵌入到前端项目中,也可以在前端启动时从后端动态获取。
  • 不需要公钥:如果前端不需要对数据进行加密,或者所有加密操作都在后端完成,那么前端不需要公钥。

密钥类型:对称与非对称

在密码学中,密钥主要分为两大类:对称密钥非对称密钥。每种类型的密钥都有其独特的特点和应用场景。

1. 对称密钥(Symmetric Key)

  • 定义:对称密钥加密使用同一个密钥进行加密和解密。加密和解密过程是对称的,即使用相同的密钥。
  • 特点
    • 速度快:对称加密算法通常比非对称加密算法更快,适合处理大量数据。
    • 密钥管理简单:只需要管理一个密钥。
    • 安全性依赖于密钥的保密性:如果密钥泄露,数据将不再安全。
  • 常见算法
    • AES(Advanced Encryption Standard):现代最常用的对称加密算法,支持多种密钥长度(如128位、192位、256位)。
    • DES(Data Encryption Standard):较老的对称加密算法,现在较少使用,因为其密钥长度较短(56位),安全性较低。
    • 3DES(Triple DES):通过三次应用DES算法提高安全性,但速度较慢。
    • Blowfish:一种可变密钥长度的对称加密算法,速度较快,安全性高。
    • Twofish:Blowfish的改进版本,支持更长的密钥长度,安全性更高。

2. 非对称密钥(Asymmetric Key)

  • 定义:非对称密钥加密使用一对密钥,即公钥和私钥。公钥用于加密数据,私钥用于解密数据。公钥可以公开分发,而私钥必须保密。
  • 特点
    • 安全性高:即使公钥被公开,私钥仍然安全,数据无法被解密。
    • 密钥管理复杂:需要管理一对密钥,公钥和私钥。
    • 速度较慢:非对称加密算法通常比对称加密算法慢,适合处理小量数据。
    • 支持数字签名:可以用于验证数据的完整性和来源。
  • 常见算法
    • RSA(Rivest-Shamir-Adleman):最常用的非对称加密算法,广泛用于安全通信和数字签名。
    • DSA(Digital Signature Algorithm):专门用于数字签名的算法,不支持加密。
    • ECDSA(Elliptic Curve Digital Signature Algorithm):基于椭圆曲线的数字签名算法,安全性高,密钥长度较短。
    • ECC(Elliptic Curve Cryptography):基于椭圆曲线的加密算法,提供相同安全级别所需的密钥长度更短,计算效率更高。

密钥存储和管理

无论是对称密钥还是非对称密钥,密钥的存储和管理都是至关重要的。以下是一些常见的存储和管理方式:

对称密钥

  • 存储:对称密钥通常存储在安全的环境中,如服务器的文件系统或数据库中。建议使用加密的方式存储密钥,以防止密钥泄露。
  • 分发:对称密钥的分发需要确保安全性,通常通过安全的通信渠道(如HTTPS)进行分发。

非对称密钥

  • 存储
    • 公钥:公钥可以公开分发,通常存储在服务器的文件系统或数据库中,也可以嵌入到前端代码中。
    • 私钥:私钥必须严格保密,通常存储在服务器的文件系统或数据库中,只有授权的应用程序和用户可以访问。
  • 分发
    • 公钥:可以通过安全的API接口动态分发给前端,也可以在前端代码中直接嵌入。
    • 私钥:私钥不应分发给前端,所有涉及私钥的操作应在服务器端完成。

总结

  • 对称密钥:速度快,适合处理大量数据,但密钥管理简单,安全性依赖于密钥的保密性。
  • 非对称密钥:安全性高,支持数字签名,但速度较慢,适合处理小量数据,密钥管理复杂。

根据具体的应用场景和安全需求,选择合适的密钥类型和管理方式。

posted @ 2025-08-22 18:22  丁少华  阅读(23)  评论(0)    收藏  举报