使用AES-256-GCM生成访问令牌时,公开拼接的IV与Auth Tag是否安全?
将IV和Auth Tag随令牌公开是否安全?
核心结论
公开IV和Auth Tag本身是安全的,但你的令牌实现存在其他需要修复的安全隐患,下面详细说明:
为什么IV和Auth Tag可以公开?
- IV(初始化向量):AES-GCM中IV的作用是确保相同明文每次加密生成不同密文,它不需要保密,只需要保证每次加密使用唯一的IV(你的代码里用
crypto.randomBytes(8)生成随机IV,满足唯一性要求)。公开IV不会泄露任何明文或密钥相关信息。 - Auth Tag(认证标签):GCM模式的Auth Tag用于验证密文的完整性和真实性,它是由密钥、IV和密文计算得出的,本身不包含明文内容。没有密钥的情况下,攻击者无法通过Auth Tag还原明文,也无法伪造有效的Auth Tag篡改密文。
你的代码存在的关键安全问题
IV长度不符合最佳实践
AES-GCM推荐使用12字节(96位)的IV,这是NIST指定的最优长度,能最大化安全性和性能。你当前使用的是8字节IV,虽然不会直接导致漏洞,但会降低加密方案的安全性上限,建议修改为:const iv = crypto.randomBytes(12).toString('hex');缺少Cookie安全属性
令牌存储在浏览器Cookie中时,必须设置以下属性防止常见攻击:HttpOnly:禁止JavaScript读取Cookie,防范XSS攻击Secure:仅在HTTPS连接下传输Cookie(生产环境必须开启)SameSite: 'strict':限制Cookie仅在同源请求中发送,防范CSRF攻击maxAge:设置与令牌过期时间一致的Cookie有效期
未强制验证令牌过期时间
你的代码在解密时只解析了expires_at,但没有主动验证当前时间是否超过过期时间。攻击者可能篡改客户端时间或Cookie有效期,导致过期令牌仍被使用。建议在decryptAccessToken函数中添加验证逻辑:const payload = JSON.parse(payloadStr); if (dayjs().isAfter(dayjs(payload.expires_at))) { throw new Error("Access token has expired"); }
完整优化后的代码示例
import crypto from 'crypto'; import dayjs from 'dayjs'; import { env } from './env'; type User = { /* 你的User类型定义 */ }; type Result<T> = { ok: true; value: T } | { ok: false; error: unknown }; type AccessTokenPayload = { created_at: string; expires_at: string; user: User; }; export const encryptAccessToken = (user: User): Result<string> => { try { // 使用推荐的12字节IV const iv = crypto.randomBytes(12).toString('hex'); const cipher = crypto.createCipheriv('aes-256-gcm', env.CIPHER_SECRET, iv); const payload = { created_at: dayjs().format('YYYY-MM-DDTHH:mm:ss'), expires_at: dayjs().add(1, 'day').format('YYYY-MM-DDTHH:mm:ss'), user, }; const cipherStr = cipher.update(JSON.stringify(payload), 'utf-8', 'hex') + cipher.final('hex'); const accessToken = [iv, cipherStr, cipher.getAuthTag().toString('hex')].join('.'); return { ok: true, value: accessToken, }; } catch (error) { return { ok: false, error }; } }; export const decryptAccessToken = (accessToken: string): Result<AccessTokenPayload> => { try { const [iv, cipher, authTag] = accessToken.split('.'); if (!iv || !cipher || !authTag) { throw new Error("Invalid access token format"); } const decipher = crypto.createDecipheriv('aes-256-gcm', env.CIPHER_SECRET, iv); decipher.setAuthTag(Buffer.from(authTag, 'hex')); const payloadStr = decipher.update(cipher, 'hex', 'utf-8') + decipher.final('utf-8'); const payload = JSON.parse(payloadStr) as AccessTokenPayload; // 强制验证令牌过期时间 if (dayjs().isAfter(dayjs(payload.expires_at))) { throw new Error("Access token has expired"); } return { ok: true, value: payload, }; } catch (error) { return { ok: false, error }; } };
内容的提问来源于stack exchange,提问作者xqcccccccccc
相关产品推荐
相关产品推荐

