从Django后端获取IV后,SvelteKit与Python加密结果不一致
问题:Django后端与SvelteKit前端AES加密结果不一致
从Django(Python)后端获取IV到SvelteKit前端,使用PyCryptodome和CryptoJS实现AES-CBC加密,密钥一致,但两端加密结果完全不同。
后端Python(Django)代码
key = bytes(os.environ['KEY'], "utf-8") iv = None def generate_iv(): rand = os.urandom(4) return struct.unpack('B' * len(rand), rand) def encrypt(username, password): global iv if iv == None: iv = generate_iv() byte_data = b''.join(struct.pack('<I', index) for index in iv) cipher = AES.new(key, AES.MODE_CBC, iv=byte_data) data = f"{username}:{password}".encode() padded_data = pad(data, AES.block_size, style='pkcs7') encrypted_data = cipher.encrypt(padded_data) print(base64.b64encode(encrypted_data).decode('utf-8')) return base64.b64encode(encrypted_data).decode('utf-8') class IV(APIView): def get(self, request): global iv if iv == None: iv = generate_iv() return Response({'iv': f'{iv}'}, status=status.HTTP_200_OK)
前端SvelteKit(JS)代码
let iv; let numbers = []; onMount(async () => { const response = await fetch("http://127.0.0.1:8000/api/authapi/iv"); const data = await response.json(); const ivNumbers = data.iv.slice(1, -1).split(",").map(Number); numbers = ivNumbers; console.log(numbers); iv = CryptoJS.lib.WordArray.create(new Uint8Array(numbers)); console.log(iv); }); function encrypt(username, password) { const key = "pR@QvK3oM&9t#S8X"; const data = `${username}:${password}`; const encryptedData = CryptoJS.AES.encrypt( data, CryptoJS.enc.Utf8.parse(key), { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: iv, } ); console.log(encryptedData.toString()); return encryptedData.toString(); }
API响应示例
{ "iv": "(246, 72, 5, 222)" }
两端加密结果
- 前端(CryptoJS):
fA6ifpnK8NCLBRi7EYPf4zyDumGFeBwVnZC0vliN06I= - 后端(PyCryptodome):
PAwNhCecq4lJRKizJWCMhknk9gleAPRXV2owj29XD2k=
问题根源
核心问题是前后端IV的处理逻辑完全不一致,导致实际使用的IV参数不同:
- 后端错误生成IV:
generate_iv生成4字节随机数,转成4个0-255的整数元组。- 但
encrypt函数中用struct.pack('<I', index)将每个整数单独打包成4字节小端整数,最终IV被转换成16字节(4个整数×4字节),这是错误的转换逻辑。
- 前端解析IV错误:
- 前端将后端返回的4个整数直接转成4字节的WordArray,实际使用的IV仅4字节,与后端的16字节IV完全不匹配。
- 额外问题:AES-CBC要求IV长度必须等于块大小(16字节),后端的错误转换碰巧凑够了16字节,但前端的IV长度不符合规范,进一步加剧了结果差异。
修正方案
1. 后端修正:生成标准16字节IV,用Base64传递
直接生成符合AES-CBC要求的16字节IV,并用Base64编码后返回,避免解析错误:
import os import base64 from Cryptodome.Cipher import AES from Cryptodome.Util.Padding import pad from rest_framework.views import APIView from rest_framework import status key = bytes(os.environ['KEY'], "utf-8") iv = None def generate_iv(): # 生成AES-CBC要求的16字节随机IV return os.urandom(16) def encrypt(username, password): global iv if iv is None: iv = generate_iv() cipher = AES.new(key, AES.MODE_CBC, iv=iv) data = f"{username}:{password}".encode() padded_data = pad(data, AES.block_size, style='pkcs7') encrypted_data = cipher.encrypt(padded_data) encrypted_b64 = base64.b64encode(encrypted_data).decode('utf-8') print(encrypted_b64) return encrypted_b64 class IV(APIView): def get(self, request): global iv if iv is None: iv = generate_iv() # 将IV转成Base64字符串返回,避免手动解析数字的麻烦 return Response({'iv': base64.b64encode(iv).decode('utf-8')}, status=status.HTTP_200_OK)
2. 前端修正:直接解析Base64格式的IV
前端无需手动解析数字,直接用CryptoJS解码后端返回的Base64 IV:
let iv; onMount(async () => { const response = await fetch("http://127.0.0.1:8000/api/authapi/iv"); const data = await response.json(); // 直接解码Base64格式的IV iv = CryptoJS.enc.Base64.parse(data.iv); console.log(iv); }); function encrypt(username, password) { const key = "pR@QvK3oM&9t#S8X"; // 确保与后端环境变量KEY完全一致 const data = `${username}:${password}`; const encryptedData = CryptoJS.AES.encrypt( data, CryptoJS.enc.Utf8.parse(key), { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: iv, } ); console.log(encryptedData.toString()); return encryptedData.toString(); }
额外注意事项
- 密钥长度校验:当前密钥
pR@QvK3oM&9t#S8X是16字符,UTF-8编码后为16字节,符合AES-128要求,若修改密钥需确保长度为16/24/32字节(对应AES-128/192/256)。 - 全局IV的安全风险:后端使用全局变量存储IV会导致多请求复用同一个IV,违反加密安全规范,建议每次加密生成新IV,并将IV与密文一起返回给前端。
- 跨域配置:若前后端域名不同,需在Django中配置CORS,避免跨域请求被浏览器拦截。
内容的提问来源于stack exchange,提问作者PepeePL
相关产品推荐
相关产品推荐

