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()
服务端(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());
前后端结合
一般业务场景是生成密钥后(运维、前端谁生成都无所谓),
然后前端拿到公钥 来加密数据
// 导入本地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接口动态分发给前端,也可以在前端代码中直接嵌入。
- 私钥:私钥不应分发给前端,所有涉及私钥的操作应在服务器端完成。
总结
- 对称密钥:速度快,适合处理大量数据,但密钥管理简单,安全性依赖于密钥的保密性。
- 非对称密钥:安全性高,支持数字签名,但速度较慢,适合处理小量数据,密钥管理复杂。
根据具体的应用场景和安全需求,选择合适的密钥类型和管理方式。