用 Javascript 加密,用 PHP 解密,使用公钥加密

Posted

技术标签:

【中文标题】用 Javascript 加密,用 PHP 解密,使用公钥加密【英文标题】:Encrypt in Javascript, decrypt in PHP, using public-key cryptography 【发布时间】:2012-09-09 13:11:41 【问题描述】:

我想在 javascript 中加密,在 php 中解密,使用公钥加密。我一直在努力寻找可以完成此任务的库,但遇到了问题。

我目前正在查看openpgpjs,但我需要所有浏览器的支持,甚至测试页面在唯一列为支持的浏览器(谷歌浏览器)上也有错误。

关于最终目标的说明:

TCP 连接已受 SSL 保护。这层保护的主要目的是防止有意或无意的网络服务器日志记录、故障转储等。

在 PHP 端,会生成一个临时私钥(它会在短时间内过期)。调用者(在 Javascript 中)负责在新的公钥过期时请求它。私钥过期的原因是为了防止记录的加密数据被解密,以防存储私钥的服务器后来被破坏。

服务器受损情况:有人获得了除数据库服务器之外的所有机器的备份(并且由于防火墙而无法访问数据库,即使他找到了用户和密码)。由于加密记录数据的私钥不复存在,攻击者无能为力。

【问题讨论】:

希望您仍然对此感兴趣...您可以使用 PHP 7.2 sodium 函数和 NaCL github.com/tonyg/js-nacl 客户端或任何类似的 libsosium javascript 实现。 【参考方案1】:

我的登录页面使用了类似的东西;它使用可以在 PHP 中解密的给定公钥信息 (N, e) 对登录凭据进行加密。

它使用以下属于JSBN 的文件:

jsbn.js - 处理大整数 rsa.js - 仅用于 RSA 加密(使用 jsbn.js) rng.js - 基本熵收集器 prng4.js - ARC4 RNG 后端

加密数据:

$pk = '-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----';
$kh = openssl_pkey_get_private($pk);
$details = openssl_pkey_get_details($kh);

function to_hex($data)

    return strtoupper(bin2hex($data));


?>
<script>
var rsa = new RSAKey();
rsa.setPublic('<?php echo to_hex($details['rsa']['n']) ?>', '<?php echo to_hex($details['rsa']['e']) ?>');

// encrypt using RSA
var data = rsa.encrypt('hello world');
</script>

这就是你将如何解码发送的数据:

$kh = openssl_pkey_get_private($pk);
$details = openssl_pkey_get_details($kh);
// convert data from hexadecimal notation
$data = pack('H*', $data);
if (openssl_private_decrypt($data, $r, $kh)) 
   echo $r;

【讨论】:

+1 为简单起见。这是一个很好且干净的解决方案。请注意,pidCrypt 使用 jsbn(格式良好且“命名空间”)。 @Tiberiu-IonuțStan 感谢您提到 pidCrypt。我为自己的项目做了命名空间,但似乎我可以节省一些时间:) @jve 虽然有些库可能没有这个要求,但建议保留这些字符串 :) 仅供参考,这默认为 PKCS1v1.5 填充,最近导致CVE in Zend\Crypt。 在第一个代码块中是 openssl_pkey_get_public(因为您将使用公钥加密)而不是 openssl_pkey_get_private【参考方案2】:

查看node-rsa。

这是一个 node.js 模块

此模块提供对来自 OpenSSL 的 RSA 公钥例程的访问。 支持仅限于 RSAES-OAEP 和使用公钥加密,使用私钥解密。

也许你可以将它移植到浏览器中运行。

更新

用于 javascript 的 RSA 客户端库:(pidcrypt 已正式停产,网站域已过期 - 请参阅 @jack 的答案,其中包含与 pidcrypt 所含相同的库)。 https://www.pidder.com/pidcrypt/?page=rsa

PHP 服务器端组件: http://phpseclib.sourceforge.net/

祝你好运!

【讨论】:

node.js 是服务器端。服务器端 javascript 模块不能在浏览器端重用,因为它依赖于 OpenSSL。 pidCrypt 仅提供对称加密或散列。我的问题是关于公钥加密。 我发布的链接指向 pidCrypt 不对称 rsa。怎么对称? 抱歉,我没有阅读支持方法列表中的 RSA 部分。现在检查一下。谢谢。 小心使用 RSA,即使使用 PHPSecLib。【参考方案3】:

在实施 RSA 时要小心。事实上,您可能根本不应该使用 RSA。 (Use libsodium instead!)

即使您使用的是库(例如直接使用 PHP 的 OpenSSL 扩展,或者直到最近,Zend\Crypt),仍然有很多地方可能出错。特别是:

PKCS1v1.5 填充是默认(在许多情况下是唯一支持的填充模式),容易受到称为填充预言的一类选择密文攻击。这是由 Daniel Bleichenbacher 首次发现的。 1998 年。 RSA 不适合加密大消息,所以实现者经常做的是将长消息分成固定大小的块,然后分别加密每个块。这不仅速度慢,而且类似于对称密钥加密的the dreaded ECB mode。

最好的办法,使用 Libsodium

在沿着这条路线走之前,您可能需要阅读 JavaScript Cryptography Considered Harmful 几遍。不过话说回来……

    将 TLSv1.2 与 HSTS 和 HPKP 一起使用,最好使用 ChaCha20-Poly1305 和/或 AES-GCM 以及 ECDSA-P256 证书(重要:当 IETF 命名 Curve25519 和 Ed25519 时,请改用该证书)。 将libsodium.js 添加到您的项目中。 在客户端使用 crypto_box_seal() 和公钥来加密您的消息。 在 PHP 中,使用\Sodium\crypto_box_seal_open() 和公钥对应的密钥来解密消息。

我需要使用 RSA 来解决这个问题。

Please don't。椭圆曲线密码学在没有侧通道的情况下更快、更简单、更容易实现。大多数图书馆已经为您这样做了。 (钠!)

但我真的想使用 RSA!

好的,请关注these recommendations to the letter,当你犯了一个错误(如SaltStack did)导致你的密码学无用时,不要向 *** 哭泣。

一个旨在提供简单易用的 RSA 加密的选项(不附带补充的 JavaScript 实现,请不要要求提供)是paragonie/easyrsa。

它通过使用 RSA-OAEP with MGF1+SHA256 而不是 PKCS1v1.5 来避免填充预言。 巧妙的协议设计避免了ECB模式:

EasyRSA 加密协议

    EasyRSA 为对称密钥加密(通过 AES)生成一个随机 128 位密钥。 您的明文消息使用defuse/php-encryption 加密。 您的 AES 密钥使用 RSA 加密,由 phpseclib 提供,使用正确的模式(如上所述)。 这些信息被打包成一个简单的字符串(带有校验和)。

但是,实际上,如果您发现公钥加密的有效用例,您需要 libsodium。

奖励:用 JavaScript 加密,用 PHP 解密

我们将使用sodium-plus 来实现这一目标。 (取自this post。)

const publicKey = X25519PublicKey.from('fb1a219011c1e0d17699900ef22723e8a2b6e3b52ddbc268d763df4b0c002e73', 'hex');

async function sendEncryptedMessage() 
    let key = await getExampleKey();
    let message = $("#user-input").val();
    let encrypted = await sodium.crypto_box_seal(message, publicKey);
    $.post("/send-message", "message": encrypted.toString('hex'), function (response) 
        console.log(response);
        $("#output").append("<li><pre>" + response.message + "</pre></li>");
    );

然后是全等的 PHP 代码:

<?php
declare(strict_types=1);
require 'vendor/autoload.php'; // Composer
header('Content-Type: application/json');
$keypair = sodium_hex2bin(
    '0202040a9fbf98e1e712b0be8f4e46e73e4f72e25edb72e0cdec026b370f4787' .
    'fb1a219011c1e0d17699900ef22723e8a2b6e3b52ddbc268d763df4b0c002e73'
);

$encrypted = $_POST['message'] ?? null;
if (!$encrypted) 
    echo json_encode(
        ['message' => null, 'error' => 'no message provided'],
        JSON_PRETTY_PRINT
    );
    exit(1);

$plaintext = sodium_crypto_box_seal_open(sodium_hex2bin($encrypted), $keypair);

echo json_encode(
    ['message' => $plaintext, 'original' => $encrypted],
    JSON_PRETTY_PRINT
);

【讨论】:

libsodium.js 的文档在哪里?如果没有记录,即使它是更好的选择,也没有人会使用它。 @RedShift github.com/jedisct1/libsodium.js#usage-as-a-module + libsodium.gitbook.io/doc @RedShift 从那以后的几个月里,我创建了一个更安全的选项,记录在 here。 @ScottArciszewski 在前端和后端使用相同的密钥更安全?这里的私钥在哪里?【参考方案4】:

pidCrypt (js) 和 phpseclib (php) 的 RSA 示例用法。

不要在此工作示例中重复使用私钥。

pidCrypt 加密

//From the pidCrypt example sandbox
function certParser(cert) 
    var lines = cert.split('\n');
    var read = false;
    var b64 = false;
    var end = false;
    var flag = '';
    var retObj = 
    ;
    retObj.info = '';
    retObj.salt = '';
    retObj.iv;
    retObj.b64 = '';
    retObj.aes = false;
    retObj.mode = '';
    retObj.bits = 0;
    for (var i = 0; i < lines.length; i++) 
        flag = lines[i].substr(0, 9);
        if (i == 1 && flag != 'Proc-Type' && flag.indexOf('M') == 0)//unencrypted cert?
        b64 = true;
        switch (flag) 
            case '-----BEGI':
                read = true;
                break;
            case 'Proc-Type':
                if (read)retObj.info = lines[i];
                break;
            case 'DEK-Info:':
                if (read) 
                    var tmp = lines[i].split(',');
                    var dek = tmp[0].split(': ');
                    var aes = dek[1].split('-');
                    retObj.aes = (aes[0] == 'AES') ? true : false;
                    retObj.mode = aes[2];
                    retObj.bits = parseInt(aes[1]);
                    retObj.salt = tmp[1].substr(0, 16);
                    retObj.iv = tmp[1];
                
                break;
            case '':
                if (read)b64 = true;
                break;
            case '-----END ':
                if (read) 
                    b64 = false;
                    read = false;
                
                break;
                default : if (read && b64)retObj.b64 += pidCryptUtil.stripLineFeeds(lines[i]);
        
    
    return retObj;


var strCreditCardPublicKey="-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC\/tI7cw+gnUPK2LqWp50XboJ1i\njrLDn+4\/gPOe+pB5kz4VJX2KWwg9iYMG9UJ1M+AeN33qT7xt9ob2dxgtTh7Mug2S\nn1TLz4donuIzxCmW+SZdU1Y+WNDINds194hWsAVhMC1ClMQTfldUGzQnI5sXvZTF\nJWp\/9jheCNLDRIkAnQIDAQAB\n-----END PUBLIC KEY-----\n";

var objParams=certParser(strCreditCardPublicKey);
var binaryPrivateKey=pidCryptUtil.decodeBase64(objParams.b64);

var rsa=new pidCrypt.RSA();

var asn=pidCrypt.ASN1.decode(pidCryptUtil.toByteArray(key));
var tree=asn.toHexTree();
rsa.setPublicKeyFromASN(tree);

var strHexSensitiveDataEncrypted=rsa.encrypt("4111111111111111");

var strBase64SensitiveDataEncrypted=pidCryptUtil.fragment(pidCryptUtil.encodeBase64(pidCryptUtil.convertFromHex(strHexSensitiveDataEncrypted)), 64))

console.log(strBase64SensitiveDataEncrypted);

.

phpseclib解密

require_once("Crypt/RSA.php");

function decrypt($strBase64CipherText)

    //CRYPT_RSA_MODE_INTERNAL is slow
    //CRYPT_RSA_MODE_OPENSSL is fast, but requires openssl to be installed, configured and accessible.
    define("CRYPT_RSA_MODE", CRYPT_RSA_MODE_INTERNAL);

    $rsa=new Crypt_RSA();


    //$strPrivateKey=file_get_contents("private.pem");
    //This private key is for example purposes
    //DO NOT REUSE
    $strPrivateKey="-----BEGIN RSA PRIVATE KEY-----
        MIICXQIBAAKBgQDBNHK7R2CCYGqljipbPoj3Pwyz4cF4bL5rsm1t8S30gbEbMnKn
        1gpzteoPlKp7qp0TnsgKab13Fo1d+Yy8u3m7JUd/sBrUa9knY6dpreZ9VTNul8Bs
        p2LNnAXOIA5xwT10PU4uoWOo1v/wn8eMeBS7QsDFOzIm+dptHYorB3DOUQIDAQAB
        AoGBAKgwGyxy702v10b1omO55YuupEU3Yq+NopqoQeCyUnoGKIHvgaYfiwu9sdsM
        ZPiwxnqc/7Eo6Zlw1XGYWu61GTrOC8MqJKswJvzZ0LrO3oEb8IYRaPxvuRn3rrUz
        K7WnPJyQ2FPL+/D81NK6SH1eHZjemb1jV9d8uGb7ifvha5j9AkEA+4/dZV+dZebL
        dRKtyHLfbXaUhJcNmM+04hqN1DUhdLAfnFthoiSDw3i1EFixvPSiBfwuWC6h9mtL
        CeKgySaOkwJBAMSdBhn3C8NHhsJA8ihQbsPa6DyeZN+oitiU33HfuggO3SVIBN/7
        HmnuLibqdxpnDOtJT+9A+1D29TkNENlTWgsCQGjVIC8xtFcV4e2s1gz1ihSE2QmU
        JU9sJ3YeGMK5TXLiPpobHsnCK8LW16WzQIZ879RMrkeDT21wcvnwno6U6c8CQQCl
        dsiVvXUmyOE+Rc4F43r0VRwxN9QI7hy7nL5XZUN4WJoAMBX6Maos2Af7NEM78xHK
        SY59+aAHSW6irr5JR351AkBA+o7OZzHIhvJfaZLUSwTPsRhkdE9mx44rEjXoJsaT
        e8DYZKr84Cbm+OSmlApt/4d6M4YA581Os1eC8kopewpy
        -----END RSA PRIVATE KEY-----
    ";
    $strPrivateKey=preg_replace("/[ \t]/", "", $strPrivateKey);//this won't be necessary when loading from PEM


    $rsa->loadKey($strPrivateKey);

    $binaryCiphertext=base64_decode($strBase64CipherText);

    $rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1);
    $strBase64DecryptedData=$rsa->decrypt($binaryCiphertext);

    return base64_decode($strBase64DecryptedData);


//The pidCrypt example implementation will output a base64 string of an encrypted base64 string which contains the original data, like this one:
$strBase64CipherText="JDlK7L/nGodDJodhCj4uMw0/LW329HhO2EvxNXNUuhe+C/PFcJBE7Gp5GWZ835fNekJDbotsUFpLvP187AFAcNEfP7VAH1xLhhlB2a9Uj/z4Hulr4E2EPs6XgvmLBS3MwiHALX2fES5hSKY/sfSUssRH10nBHHO9wBLHw5mRaeg=";

$binaryDecrypted=decrypt($strBase64CipherText);

//should output '4111111111111111'
var_export($binaryDecrypted);

【讨论】:

【参考方案5】:

这是基于Tiny Encryption Algorithm,这是一个对称(私钥)加密系统。尽管如此,由于它的重量轻,它可能对您有用。

现在在:http://babelfish.nl/Projecten/JavascriptPhpEncryption

【讨论】:

该网站似乎有点过时......正如你所说......算法是对称的(这意味着记录的数据很容易破坏,如果密钥也被记录)。我什至不确定这是跨浏览器还是它是否适用于 UTF-8 和字节数组。无论如何谢谢:) 谢谢托尼。我更新了网站,但没有注意到这一点。这东西现在在babelfish.nl/Projecten/JavascriptPhpEncryption

以上是关于用 Javascript 加密,用 PHP 解密,使用公钥加密的主要内容,如果未能解决你的问题,请参考以下文章

php中的AES加密,然后用Javascript(cryptojs)解密

java加密用PHP解密

用 CryptoJS 加密,用 PHP 解密

怎么用php进行3des解密

php 怎么用zend加密

C#如何解密用javascript中encodeURIComponent函数加密过的数据