桌面应用相比纯网页有一个显著差异:用户数据往往直接存储在本地磁盘上,一旦应用被反编译或电脑被他人物理接触,这些数据就可能面临泄露风险。因此,必须在设计阶段就规划好敏感数据的保护策略,而不是等问题发生后再补救。
24.4.1 本地存储加密
不要在本地明文保存任何敏感数据,尤其是这些内容:
- 用户身份凭证(access token、refresh token、session key)
- 第三方 API 密钥
- 用户隐私内容(聊天记录、笔记、财务信息)
最常用的本地加密方案是结合系统原生安全机制和成熟加密库:
使用 safeStorage 保护小量数据
Electron 提供了 safeStorage 模块,它会在 macOS 上使用 Keychain,在 Windows 上使用 DPAPI,在 Linux 上使用 libsecret。适合保存 token 类型的短字符串:
const { safeStorage } = require('electron');
// 保存
const plainText = 'user-refresh-token';
const encrypted = safeStorage.encryptString(plainText);
fs.writeFileSync('token.dat', encrypted);
// 读取
const buffer = fs.readFileSync('token.dat');
const decrypted = safeStorage.decryptString(buffer);
优点:加密密钥由操作系统管理,很难被提取。缺点:不适合大文件,且迁移到不同机器后无法解密(这是安全特性,也能通过用户登录恢复)。
对较大数据使用 AES 加密
如果需要加密整个配置文件或数据库,可以使用 Node.js 的 crypto 模块进行 AES-256-GCM 加密。密钥不能写死在代码里,应该通过用户提供的密码派生:
const crypto = require('crypto');
const algorithm = 'aes-256-gcm';
function deriveKey(password, salt) {
return crypto.scryptSync(password, salt, 32);
}
function encrypt(text, password) {
const salt = crypto.randomBytes(16);
const key = deriveKey(password, salt);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([salt, iv, tag, encrypted]); // 只需安全存储这个 buffer
}
实际项目中,建议直接使用具备安全审计的加密库(如 libsodium-wrappers),它封装好了算法选择、nonce 管理等复杂细节,降低误用风险。
敏感数据库加密
如果应用使用了 SQLite,推荐使用 @journeyapps/sqlcipher 或 better-sqlite3 的加密扩展,直接对数据库文件整体加密。这样即便文件被拷贝走,没有密码也无法读取内部数据。
24.4.2 密码保护
当应用需要用户输入密码时(比如解锁应用、加密解密数据),密码本身绝不能以明文方式持久化。正确的处理链路是:
- 用户在界面中输入密码
- 立即通过
crypto.scrypt或pbkdf2派生为密钥(可以同时生成一个校验哈希) - 只存储校验哈希,丢弃原始密码
验证密码时重复派生过程,比对哈希值是否一致。示例:
const crypto = require('crypto');
function hashPassword(password, salt) {
return new Promise((resolve, reject) => {
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) reject(err);
resolve(derivedKey.toString('hex'));
});
});
}
// 创建用户密码时
const salt = crypto.randomBytes(16).toString('hex');
const hash = await hashPassword(userInput, salt);
// 保存 salt 和 hash 到本地文件或数据库
// 验证时
const newHash = await hashPassword(userInput, savedSalt);
const isValid = newHash === savedHash;
此外还应做到:
- 限制密码尝试次数(比如连续失败 5 次后锁定 30 秒),防止暴力破解。
- 不要在日志中输出密码明文或派生的密钥。
- 当用户切换应用或锁屏时,如果处于敏感页面,应要求重新输入密码或生物识别验证。
24.4.3 日志脱敏
桌面应用通常需要记录日志以便排查问题,但日志中很容易无意间写入敏感信息:API 响应包含 token、请求参数里有身份证号、甚至用户复制的内容都可能被错误记录。因此必须建立日志脱敏机制。
实用脱敏策略
- 使用日志级别隔离:生产环境默认只记录
info及以上,debug仅在用户手动开启后使用。 - 自动过滤敏感字段:在日志输出前,根据字段名(如
password、token、secret、authorization)替换值:
const sensitiveKeys = ['password', 'token', 'secret', 'authorization'];
function sanitize(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const cleaned = {};
for (const [key, value] of Object.entries(obj)) {
if (sensitiveKeys.includes(key.toLowerCase())) {
cleaned[key] = '***';
} else {
cleaned[key] = sanitize(value);
}
}
return cleaned;
}
- 避免记录完整 HTTP 请求:如果使用 axios 等库,配置拦截器自动脱敏
axios.interceptors.request.use(config => {
const logData = { method: config.method, url: config.url };
// 不记录 headers 或只记录白名单字段
return config;
});
- 正则匹配高权限数据:对可能出现的身份证、手机号、邮箱等字符串,用正则替换中间部分为星号。例如手机号保留前 3 后 4 位,其余脱敏。
- 远程日志上传同样必须脱敏:很多应用会将错误日志上传到服务器,这属于高风险操作,必须在上传前做一遍完整的脱敏,甚至完全禁止上传含用户内容的日志。
日志文件本身的保护
本地日志文件应放在应用专属目录(app.getPath('logs') 或 app.getPath('userData') 下),并使用操作系统级别的权限隔离。如果日志包含极为敏感的信息,可以考虑用 safeStorage 对日志文件加密,或者在日志轮转时自动删除超过 7 天的旧日志。
小结:敏感数据安全不是一个单一的技术点,而是一套贯穿存储、交互、运维的习惯。原则就是默认不信任任何本地文件,默认不记录任何隐私数据。在实现每一项功能时都多问自己一句:这个数据如果被拷贝走,会造成多大危害?根据答案选择对应的加密、脱敏和访问控制策略。这样搭建出来的桌面应用,才能面对真实的用户环境依然稳固可靠。