在 JavaScript 中解密 OAEP RSA(如 PHP 的 openssl_private_decrypt)

Posted

技术标签:

【中文标题】在 JavaScript 中解密 OAEP RSA(如 PHP 的 openssl_private_decrypt)【英文标题】:Decrypting OAEP RSA In JavaScript (Like PHP's openssl_private_decrypt) 【发布时间】:2022-01-21 07:15:54 【问题描述】:

我目前在 php 中加密数据如下:

//$sMessage is the input to encrypt.
//$sPublicKey is the public key in PEM format.
openssl_public_encrypt($sMessage, $sEncrypted, $PublicKey, OPENSSL_PKCS1_OAEP_PADDING);
//$sEncrypted will now store the resulting text.

以下是 PHP 中用于生成密钥的代码示例:

  $aConfig = array(
    "digest_alg" => "sha256",
    "private_key_bits" => 4096,
    "private_key_type" => OPENSSL_KEYTYPE_RSA,
  );
  $res = openssl_pkey_new($aConfig);
  //$sPassPhrase is the pass phrase.
  openssl_pkey_export($res, $sPrivateKey, $sPassPhrase);
  //$sPrivateKey is the key (in PEM format).

我的解密是使用受密码保护的私钥。

//$sPrivateKeyWithPassPhrase is the protected private key in PEM format.
//$sPassPhrase is the pass phrase to protect the key.
$newRes = openssl_pkey_get_private($sPrivateKeyWithPassPhrase, $sPassPhrase);
//$sEncrypted is the encrypted text (ciphertext).
openssl_private_decrypt($sEncrypted, $sDecrypted, $newRes, OPENSSL_PKCS1_OAEP_PADDING);
//$sDecrypted is the decrypted text (plaintext)

我还发现了如何使用密码库 (which you can learn about here) 使用 Python 进行解密:

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

#passphrase is the pass phrase
passphrase_bytes = bytes(passphrase, 'utf-8')
#privatekey is the encrypted private key
private_key = serialization.load_pem_private_key(
    privatekey,
    password=passphrase_bytes,
)
#ciphertext is the text to be decrypted
plaintext = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA1()),
        algorithm=hashes.SHA1(),
        label=None
    )
)
#plaintext will be the decrypted result.

但是,我一直在努力在 javascript 中找到类似的函数。

目前我找到的最佳答案是这个:RSA Encryption Javascript

但是,密钥似乎不是 PEM 格式,当我去 pidCrypt 主页获取更多文档时,这是一个 404 错误。

我也找到了这个:PHP's openssl_sign Equivalent in Node JS

总的来说,NodeJS 库似乎具有正确的功能,但是当我浏览 NodeJS 文档时,Quick Start Guide 似乎暗示要使用 NodeJS,我必须首先安装软件并设置 Web 服务器。根据how to run node.js client on browser,不可能“Node.js 不是浏览器 javascript。它的许多部分使用浏览器上下文中不可用的操作系统功能。” (我希望我可以在客户端的本机浏览器中运行一些东西。)

有人在评论中建议我可以使用 WebCrypto。经过一番努力弄清楚在哪里下载它,我发现它实际上是浏览器的原生(这是一个惊喜)。但是,我无法弄清楚如何从文档中导入 PEM 密钥,当我生成密钥然后导出它时,唯一可用的格式是“jwk”(某种 JavaScript 对象),“ spki”(某种数组缓冲区)和“pkcs8”(我只得到 DOMException: key is not extractable)。没有什么看起来像 OpenSSL PEM 格式。 (我相信这是 PKCS1。)

当我执行“crypto.subtle.importKey”时,它需要 5 个不同的参数(格式、keyData、算法、可提取、keyUsages)。这些都不接受 PEM 字符串。经过一番挣扎,我确实找到了SubtleCrypto importKey from PEM,它确认如果您进行花哨的操作来导入 PEM 公钥,这是可能的,但是这是针对 PEM 格式的公钥,我不知道如何处理密码短语(实际上我'宁愿不要手动编程)。然后我找到了How can I import an RSA private key in PEM format for use with WebCrypto? 和Javascript, crypto.subtle : how to import RSA private key? 但是那些基本上告诉我这是不可能的,除非我去使用命令行 OpenSSL 将私钥转换为 PKCS8(这也必须单独安装,我记得这是一个巨大的痛苦在 Windows 上)。一旦我克服了这个障碍,我仍然不确定如何在 PKCS8 中获得等效的密码保护。我希望有类似的东西可以理想地支持 PKCS1 来执行与上面的 PHP 和 Python 代码相同的操作,并且我可以在本机浏览器中 100% 运行它。 (导入更多的 JavaScript 代码是可以的。)

理想情况下,我希望找到一个简单的用于 OpenSSL RSA 解密的 JavaScript 示例,相当于上面的 PHP 或 Python 示例。如果我有私钥 PEM 字符串、密码短语和密文,它可以返回明文。

非常感谢!非常感谢您的所有帮助,您可以为我节省很多时间!

【问题讨论】:

如何将我的私钥(PEM 格式,它是一个字符串)加载到这些系统之一?当我在那里生成密钥时,似乎无论我指定什么,导出都是一个有趣的复杂对象,看起来不像 OpenSSL PEM 格式,我不知道如何使用它。我将编辑问题以澄清我不是在寻找库建议,而是在任何维护的库中与 PHP 或 Python 等效的工作示例。 好的,我又花了一个小时试图让 NodeJS 或 WebCrypto 工作,并将我的具体问题添加到问题中。同样,我对任何与 PHP 或 Python 示例相同的有效 JavaScript 示例感到满意。 NodeJS 看起来完全有我需要的功能,但是我不知道如何在浏览器中运行它。根据***.com/questions/28059521/…,不可能“Node.js 不是浏览器 javascript。它的许多部分使用浏览器上下文中不可用的操作系统功能。”明确地说,当我说 JavaScript 时,我的意思是能够在浏览器中解密,这样我就可以拥有一个包含一些 JavaScript 的 html 文件,而无需安装任何自定义软件。 至于 SubtleCrypto,我相信我最终可以弄清楚如何手动解密它。令我惊讶的是,没有一个简单的标准库或函数可以为我做到这一点。我认为找到一个(如果存在的话)会更快,但我还没有遇到过。为什么它不向后兼容 PKCS1?必须有一个更好的解决方案,我错过了。 我知道,在幕后,这些算法有很多复杂性,而且我知道构建和测试框架需要时间,而且许多密码学家在使事情变得简单或易于使用时遇到了麻烦.但我认为,每当出现隐私泄露或某人的数据被泄露或主要组织滥用人们的个人信息时,它可能会成为帮助使密码学更易于访问的一些动力,以便更多的项目和个人能够更好地理解和正确保护他们的信息. 【参考方案1】:

PKCS#1 与 PKCS#8

请注意,虽然在 PHP 中生成的没有密码短语的私钥是 PKCS#1,但从 PHP 生成的带有密码短语的私钥实际上是 PKCS#8(请参阅this link,了解如何确定哪个标准适用于私钥)。

需要第三方库

为了使用 PEM 格式的密钥,需要自定义 JavaScript 库(或其他自定义代码)。 There are various libraries at this link which you can look at. 很多都不再维护了。

此示例使用 Forge JavaScript 库,但您也可以使用其他库。

安装/导入 Forge

虽然有various ways to build the Forge environment,但最简单的方法是在需要进行加密/解密的位置之前添加以下 HTML 代码:

<script src="https://cdn.jsdelivr.net/npm/node-forge@0.7.0/dist/forge.min.js"></script>

执行 RSA 解密

以下 JavaScript 代码将使用 Forge 库和受密码保护的私钥执行解密:

// Import an PKCS#8 encrypted key from PHP
//sEncPkcs8Pem = The encrypted PEM key in a string.
//sPassPhrase = A pass phrase string to use for decryption.
//bCipherText = The encrypted cipher text (string of bytes).
var encryptedPrivateKeyInfo = forge.pki.encryptedPrivateKeyFromPem(sEncPkcs8Pem);
var privateKeyInfo = forge.pki.decryptPrivateKeyInfo(encryptedPrivateKeyInfo, sPassPhrase);
var pkcs8Pem = forge.pki.privateKeyInfoToPem(privateKeyInfo);
var privateKey = forge.pki.privateKeyFromPem(pkcs8Pem);
var sDecrypted = privateKey.decrypt(bCipherText, 'RSA-OAEP');
//sDecrypted will store the result.

完整的工作示例

以下是使用 4096 位密钥的完整工作示例。 JavaScript 导入 加密 私钥(PKCS#8 格式,PEM 编码)并成功解密密文。 加密密钥和密文都是使用发布的 PHP 代码生成的。 应用的 JavaScript 库是forge

<script src="https://cdn.jsdelivr.net/npm/node-forge@0.7.0/dist/forge.min.js"></script>

<p style="font-family:'Courier New', monospace;" id="pt"></p>

<script>
var encPkcs8Pem = `-----BEGIN ENCRYPTED PRIVATE KEY-----
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
ij9mm4GhutigIoP4Wx/NUA==
-----END ENCRYPTED PRIVATE KEY-----`; // 4096 bit key!

// Import an PKCS#8 encrypted key from PHP
var encryptedPrivateKeyInfo = forge.pki.encryptedPrivateKeyFromPem(encPkcs8Pem);
var privateKeyInfo = forge.pki.decryptPrivateKeyInfo(encryptedPrivateKeyInfo, prompt("Enter pass phrase", "testkey"));
var pkcs8Pem = forge.pki.privateKeyInfoToPem(privateKeyInfo);
var privateKey = forge.pki.privateKeyFromPem(pkcs8Pem);

// Decrypt ciphertext from PHP
var ciphertext = forge.util.decode64("lHIspL7aAlnbUdQn6VVY2AS3Ybs/H0fK7N4owfPe6OtzMmX88XOI+2FyEnNyCal6krnfdHYa2JWeWAsyLf484sT2BmvNPxWyYsrnpulKog0lhkeUFppGdhdOHCf7KU47L5p6uYtZmv926ACfhwqMo4M66n/Gkliocol+esavVcfZcjuAw+ELnYkx/TufinR772jBLxVWueGyGcEokI9osDCDpXptmJqpiNRzIrf2kHdk2F/bXRPyE+vrQKFzbyMSpBj3xKtsYDiJ5xDq0qtYY+GCRJNMucPFqOTiFN18EE3z8y/tq5n9Ae/VS8wMxn51rB5JBRmYg3vlM4JLolqNnP3t8OD+DqAeavYjAnairLV0esxzjdkUDpfmFrsYBZWfe5rMzO55drDdsQpGI9LGeX5/TUQpqJWBNZ7QXnbxEiQ2ATXH+ToaeZe3mMkPKuogKM2aEViLzdYB0plbQnoWPAG4OH9pYPGj1KYwdQG5UoF0Ew6Y93uSG3uJ6mcdZZ9gJA0+hpVHOchuAvtZ+vuIV4tzWGfvvnAND54U5sS95EGRj8w/ukgswgOfg/k9iKN3Xlh5N23BYyoLajZVN92cVw6I3XpNOWjlHcO04EhLXaxIeaEMsYYqq3GDBtxpbXfARVjokMeKkKUKNwVscpVneq6WXGtfiMGwXpF8MfoGigE=");
var decrypted = privateKey.decrypt(ciphertext, 'RSA-OAEP');
document.getElementById("pt").innerHTML = "Decrypted: " + decrypted;
</script>

注意:我要感谢 Topaco 提供的大量帮助和原创答案,这是本文的基础。

【讨论】:

以上是关于在 JavaScript 中解密 OAEP RSA(如 PHP 的 openssl_private_decrypt)的主要内容,如果未能解决你的问题,请参考以下文章

使用Java和JavaScript之间的OAEP进行RSA加密

RSA OAEP、Golang 加密、Java 解密 -BadPaddingException:解密错误

如何使用SunMSCAPI密钥解密RSA-OAEP

如何在 Crypto++ 中使用 RSA OAEP SHA-256 加密/解密数据

RSA 加密然后解密失败并出现“oaep 解码错误”

golang Golang RSA-OAEP加密和解密