You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django+Web Crypto API实现RSA密钥验证时解密失败求助

RSA-OAEP解密失败:Django与Web Crypto API交互问题

我在开发Django应用时,实现了一套基于RSA挑战机制的用户私钥验证流程:

  • 客户端通过Web Crypto API生成RSA密钥对,将公钥以JWK格式发送给Django后端
  • 私钥由用户本地保存为PEM文件
  • 预期流程:Django生成UUID并通过公钥加密,发送给客户端;客户端加载本地私钥解密UUID后回传,完成认证

目前各环节都正常,唯独客户端解密时出现异常。

Django后端代码

import secrets
import json
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256

# 假设base64url_to_base64是处理URL安全Base64转标准Base64的函数
uuid = secrets.token_hex(16)
key = json.loads(request.POST.get('key'))  # key是JWK格式

e = int.from_bytes(base64.b64decode(base64url_to_base64(key['e'])), "big")
n = int.from_bytes(base64.b64decode(base64url_to_base64(key['n'])), "big")
rsakey = RSA.construct((n, e), consistency_check=True)

cipher = PKCS1_OAEP.new(rsakey, SHA256)
challenge = cipher.encrypt(uuid.encode())
challenge = base64.b64encode(challenge)

客户端JavaScript代码

fileReader.readAsText(file);
fileReader.onload = function() {
  filekey = fileReader.result;
  filekey = filekey.substring(filekey.indexOf('-----BEGIN PRIVATE KEY-----'));

  let pem = filekey;
  const pemHeader = "-----BEGIN PRIVATE KEY-----";
  const pemFooter = "-----END PRIVATE KEY-----";
  const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length);
  const binaryDerString = window.atob(pemContents);
  const binaryDer = str2ab(binaryDerString);

  // 原处理逻辑
  challenge = str2ab(window.atob(challenge.substring(2, challenge.length - 1)));
  console.log(challenge);

  window.crypto.subtle.importKey(
    "pkcs8",
    binaryDer,
    {
      name: "RSA-OAEP",
      hash: {name: "SHA-256"}
    },
    true,
    ["decrypt"]
  ).then((key) => {
    window.crypto.subtle.decrypt(
      {
        name: "RSA-OAEP"
      },
      key,
      challenge
    ).then((txt) => {
      console.log(txt);
      console.log(ab2str(txt));
    });
  });
}

// 假设str2ab和ab2str是处理字符串与ArrayBuffer互转的工具函数
function str2ab(str) {
  const buf = new ArrayBuffer(str.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint8Array(buf));
}

错误信息

客户端调用window.crypto.subtle.decrypt()时抛出异常:

Uncaught (in promise) DOMException: The operation failed for an operation-specific reason

问题排查与解决

最初怀疑是密文格式不兼容(PyCryptodome的PKCS1_OAEP.encrypt()输出字节串,Web Crypto API需要ArrayBuffer),或者Django端通过JWK构造公钥时出错。

经过测试验证,最终发现问题出在Django返回的Base64字符串上:Python的base64.b64encode()返回的是字节对象(如b'abcdef...'),直接返回给客户端后,字符串会带有b''包裹。客户端用atob()处理时,无法识别这些多余字符,导致解密失败。

修正方案

  1. 后端修正:在Django中将Base64字节对象转为普通字符串:
challenge = base64.b64encode(challenge).decode('utf-8')
  1. 客户端修正:如果后端没处理,确保客户端拿到纯Base64字符串后再解码,比如去掉b'前缀和'后缀:
// 确保challenge是纯Base64字符串,没有b''包裹
const cleanChallenge = challenge.replace(/^b'|'$/g, '');
challenge = str2ab(window.atob(cleanChallenge));

修正后,客户端可以正常解密UUID,完成认证流程。

内容的提问来源于stack exchange,提问作者Randusr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 14:05:21