解密使用RSA-OAEP在JavaScript中加密的C#中的数据时,OAEP填充错误

问题描述:

在我不深入讨论细节之前,我要完成的高级工作是使用JavaScript加密一些数据,将其发送到Web服务器,然后使用C#解密该加密的数据.我遇到的问题是解密C#中的数据.

Before I get too much into the details, the high level thing I'm trying to accomplish is encrypting some data in JavaScript, sending that to a web server, then decrypting that encrypted data in C#. The part I'm having trouble with is decrypting the data in C#.

我正在像这样用JavaScript加密一些数据(我删除了多余的代码):

I'm encrypting some data in JavaScript like this (I removed the extraneous code):

// https://github.com/diafygi/webcrypto-examples#rsa-oaep---encrypt
window.crypto.subtle.encrypt(
    {
        name: "RSA-OAEP"
    },
    publicKey,
    data
)
.then(function (encrypted) {
    // ...
});

我确认可以像这样用JavaScript对其进行解密(请注意,我实际上并不想这样做,但是我这样做是为了证明可以解密数据):

I confirmed that I can decrypt it in JavaScript like so (note that I don't actually want to do this, but I did it to prove that the data could be decrypted):

function decryptValue () {

    // Base64 decode the encrypted data for the value "Bob".
    var data = base64Decode("CthOUMzRdtSwo+4twgtjCA674G3UosWypUZv5E7uxG7GqYPiIJ+E+Uq7vbElp/bahB1fJrgq1qbdMrUZnSypVqBwYnccSxwablO15OOXl9Rn1e7w9V9fuMxtUqvhn+YZezk1623Qd7f5XTYjf6POwixtrgfZtdA+qh00ktKiVBpQKNG/bxhV94fK9+hb+qnzPmXilr9QF5rSQTd4hYHmYcR2ljVCDDZMV3tCVUTecWjS5HbOA1254ve/q3ulBLoPQTE58g7FwDQUZnd7XBdRSwYnrBWTJh8nmJ0PDfn+mCTGEI86S7HtoFYsE+Hezd24Z523phGEVrdMC9Ob1LlXEA==");

    // Get private key.
    var keyPromise = importPrivateKey();
    return keyPromise.then(function (privateKey) {

        // Decrypt the value.
        return window.crypto.subtle.decrypt(
            {
                name: "RSA-OAEP"
            },
            privateKey,
            data
        )
        .then(function (decrypted) {

            // Log the decrypted value to the console.
            console.log(arrayBufferToString(decrypted));

        });

    });

}

为简单起见,该代码示例正在解密先前加密的"Bob"值.效果很好.

For simplicity, that code sample is decrypting a previously encrypted value of "Bob". This works fine.

当我尝试解密C#中的值时出现问题:

The problem occurs when I try to decrypt the value in C#:

public static string Decrypt()
{

    // The encrypted and base64 encoded value for "Bob".
    var encryptedValue = "CthOUMzRdtSwo+4twgtjCA674G3UosWypUZv5E7uxG7GqYPiIJ+E+Uq7vbElp/bahB1fJrgq1qbdMrUZnSypVqBwYnccSxwablO15OOXl9Rn1e7w9V9fuMxtUqvhn+YZezk1623Qd7f5XTYjf6POwixtrgfZtdA+qh00ktKiVBpQKNG/bxhV94fK9+hb+qnzPmXilr9QF5rSQTd4hYHmYcR2ljVCDDZMV3tCVUTecWjS5HbOA1254ve/q3ulBLoPQTE58g7FwDQUZnd7XBdRSwYnrBWTJh8nmJ0PDfn+mCTGEI86S7HtoFYsE+Hezd24Z523phGEVrdMC9Ob1LlXEA==";

    // Assuming RSA-OAEP.
    var doOaep = true;

    // Setup encryption algorithm.
    var provider = GetPrivateKey();

    // Decrypt value.
    var encryptedData = Convert.FromBase64String(encryptedValue);
    // This line throws an error: "Error occurred while decoding OAEP padding."
    var decryptedData = provider.Decrypt(encryptedData, doOaep);
    var decryptedText = Encoding.Unicode.GetString(decryptedData);

    // Return decrypted text.
    return decryptedText;

}

显示 provider.Decrypt(encryptedData,doOaep)的行将引发错误,并显示一条消息解码OAEP填充时发生错误".堆栈跟踪为:

The line that says provider.Decrypt(encryptedData, doOaep) throws an error with a message of "Error occurred while decoding OAEP padding." The stack trace is:

Error occurred while decoding OAEP padding.
    at System.Security.Cryptography.RSACryptoServiceProvider.DecryptKey(SafeKeyHandle pKeyContext, Byte[] pbEncryptedKey, Int32 cbEncryptedKey, Boolean fOAEP, ObjectHandleOnStack ohRetDecryptedKey)
    at System.Security.Cryptography.RSACryptoServiceProvider.Decrypt(Byte[] rgb, Boolean fOAEP)

似乎JavaScript加密值的方式与C#加密值的方式不兼容.在我完全放弃这种方法并尝试使用另一个JavaScript库进行加密之前,是否有解决此错误的方法?

It seems like maybe the way the JavaScript is encrypting the value is not compatible with the way the C# is encrypting the value. Before I completely abandon this approach and try another JavaScript library for encryption, is there some way around this error?

对于其他上下文,我猜测此错误与本文中提到的内容有关:

For additional context, I am guessing this error is related to something mentioned in this article: https://www.codeproject.com/Articles/11479/RSA-Interoperability-between-JavaScript-and-RSACry

它说:

JavaScript代码中不兼容的填充方案会产生服务器端出现错误数据"异常.

Incompatible padding scheme from the JavaScript code would produce the "bad data" exception at the server side.

因此,JavaScript代码需要实现两个填充之一.NET RSA实现中使用的方案,第一个是PKCS#1 v1.5填充,另一个是OAEP(PKCS#1 v2)填充.

The JavaScript code therefore needs to implement one of two padding schemes used in the .NET RSA implementation, the first is PKCS#1 v1.5 padding and another is OAEP (PKCS#1 v2) padding.

我没有得到确切的异常,但是也许自从写这篇文章以来,错误消息已经改变了.无论如何,该文章所说的内容似乎暗示着JavaScript的加密方式与C#的解密方式不兼容(即,由于C#对填充的要求).

I'm not getting that exact exception, but maybe since that article was written the error message has changed. In any event, what that article says seems to imply that the way the JavaScript is encrypting isn't compatible with the way the C# is decrypting (namely, due to C#'s requirement for padding).

我缺少什么吗?是否有一些参数或一些简单的方法可以使加密在JavaScript中工作,而解密在C#中工作?也许有一些C#库以与JavaScript加密方式兼容的方式解密?

这是一个完整的示例,显示JavaScript正确解密(仅在某些浏览器上有效……可能无法在IE上运行):

Here's a full example that shows the JavaScript is decrypting properly (only works on some browsers... probably not going to work on IE):

function decryptValue () {

    // Base64 decode the encrypted data for the value "Bob".
    var data = base64Decode("CthOUMzRdtSwo+4twgtjCA674G3UosWypUZv5E7uxG7GqYPiIJ+E+Uq7vbElp/bahB1fJrgq1qbdMrUZnSypVqBwYnccSxwablO15OOXl9Rn1e7w9V9fuMxtUqvhn+YZezk1623Qd7f5XTYjf6POwixtrgfZtdA+qh00ktKiVBpQKNG/bxhV94fK9+hb+qnzPmXilr9QF5rSQTd4hYHmYcR2ljVCDDZMV3tCVUTecWjS5HbOA1254ve/q3ulBLoPQTE58g7FwDQUZnd7XBdRSwYnrBWTJh8nmJ0PDfn+mCTGEI86S7HtoFYsE+Hezd24Z523phGEVrdMC9Ob1LlXEA==");

    // Get private key.
    var keyPromise = importPrivateKey();
    return keyPromise.then(function (privateKey) {

        // Decrypt the value.
        return window.crypto.subtle.decrypt(
            {
                name: "RSA-OAEP"
            },
            privateKey,
            data
        )
        .then(function (decrypted) {

            // Log the decrypted value to the console.
            console.log("Decrypted value: " + arrayBufferToString(decrypted));

        });

    });

}

function importPrivateKey() {
    var rawKey = {
        "alg": "RSA-OAEP-256",
        "d": "E4KDwgxy7jFrqeXqKjxPTGOdbEoZ2aWj5qcZhUJcnr9Qh_jg_grkgpHVwEbQifTxsipXTiR3_ygspI4XFoeV-wDVfWqWCVR3_bHChF9PW8Ak1x_dBSS28BMs8PdthI1pDbpqPhmMcF4riHCtNo1M1v8cLdeaiqiXitNVBkaTePsDiucfwOy1rgxwBqAL1CNJhP8oRiYkxD-gfE_EapWuXY9-wF9O-lXPLSTKWgMmmVxSmhUP-Uqk7cJ24UH9C7W7hnSQU4pkfD5XHx3_2WO2GMKKZcqz39wJUrQzrIO7539SYsQ3rEe4aMJyL4U-Ib4_purzVS0DRjzGxK8chT2guQ",
        "dp": "kibhWHk1R6yBlhZbjIrNl9beAkyV5vtFsj_F0ixbIITzjSqI_td71sWjKQvJ2rR7hu5DYTZ4p3XwBeQ2jpYQV-y5uh4v7rGngh-0GHuHqMiUQnejgYGcHgng4iCM4e3aTO7QUlP8jqRfxw6xpfNTjrVbAL8LtdCG21vmqOiLkXE",
        "dq": "qLF9x-zKfaXlLsNgBQ1ZnaQexrnJRqrRh9JSU85fCNy5mmpKWAUbCHB-59CGAId8wMAnAyEpjcBOKNTqWSlNzp84xeUHcyPI-Dt4Yp_Y_dXjGAYntALSJs4qeF2rk55MSpiSD_KSU4DknX_E_G2rFMY7AZOSwi1D8YcNmj5okTE",
        "e": "AQAB",
        "ext": true,
        "key_ops": [
            "decrypt"
        ],
        "kty": "RSA",
        "n": "oQeTwOlTc6rIb2kddwIOc0Ywslc7YzJSRZd_PegW7T3nO3DqCI5kp5EJmnGP8JJ9sbyVYyAHFLZQtMP69UspZFn__fBk2LTp2QdqBSMHbObENcSiG2FH-pZSwCaj3Pvy-qvTjnkxxN-3OE6oB8EcX5ekZwCZzAxazbVXctY_hCcaTWG7ugwc_ZyvhsdE7wa3pnTfXYHWXcDDT8FTpYl62aqWsEIUAJSkgmQ9zce0RiDUjBJyJEM9P0ihp1Ab8BD88pEM22-PXfiOesRzp5yOsjzI3kdr5KPsshstneJEGHYae5GZXLUpnVMRY1TCFFLbkPwK6oVkRaVU1RvK9ssO3Q",
        "p": "2TTEToB4AuPIPPpg3yTyBlGb_m-f4r-TxpU96ConV2p696_4QI6jlPWwgcC9Vdma_Da43AGuyLzIptgkzF8nSjV80VwwDKQ1YkFPc6ZqB2isvExuieSP6_jLlB-fCyCLqtTxpPm2VcK16Pqm0s5T0QGH6cQjjm1r2Ww1wuaiQbk",
        "q": "vcpFwkZKZ3hx3FpHgy3ScuuTRSPO2ge8TE8UMJdCrEnpftAeYuVYrJqnxfzKgyl02OijAUi1eozJxj_lM5McxrKZEEAvo6e8wtzl2hnkUh-KWoBJ8ii0VJcu6U5vs4pcv-lYBPFC6fzoGnUw8LNWMxb5ejgYbLUWp10BbfkWGEU",
        "qi": "Mza7JYleki7BvmD3dX5CO2nkD3mBGz4_0P_aoWyHEkWu4p5XWillaRVWyLnQEubLvAduUCr-lhfNmzdUhHecpE438_LQNtKRyOq9zkvjsMOGDmbkKpZ7-aTSshax6KNlYOWdOkadjuLtRExCmwbzu5lgI4NwacxSs5MfjHMrTCo"
    };
    return window.crypto.subtle.importKey(
        "jwk",
        rawKey,
        {
            name: "RSA-OAEP",
            hash: { name: "SHA-256" }
        },
        true,
        ["decrypt"]
    );
}

function arrayBufferToString(buffer) {
    var result = '';
    var bytes = new Uint8Array(buffer);
    for (var i = 0; i < bytes.length; i++) {
        result += String.fromCharCode(bytes[i]);
    }
    return result;
}

// Decodes a base64 encoded string into an ArrayBuffer.
// https://*.com/a/36378903/2052963
function base64Decode(base64) {
    var binary_string = window.atob(base64);
    return stringToArrayBuffer(binary_string);
}

// Converts a string to an ArrayBuffer.
function stringToArrayBuffer(value) {
    var bytes = new Uint8Array(value.length);
    for (var i = 0; i < value.length; i++) {
        bytes[i] = value.charCodeAt(i);
    }
    return bytes.buffer;
}

decryptValue();

顺便说一句,我的一些代码示例显示了我正在使用的私钥.这是为了帮助您理解代码(这是一个扔掉的钥匙).实际上,这就是我在C#中获取私钥的方法:

BTW, some of my code samples show the private key I'm using. That's intentional to help you understand the code (it's a throw away key). In fact, here's how I am getting the private key in C#:

private static RSACryptoServiceProvider GetPrivateKey()
{
    RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
    RSAParameters RSAparams = new RSAParameters();
    RSAparams.Modulus = Base64UrlDecode("oQeTwOlTc6rIb2kddwIOc0Ywslc7YzJSRZd_PegW7T3nO3DqCI5kp5EJmnGP8JJ9sbyVYyAHFLZQtMP69UspZFn__fBk2LTp2QdqBSMHbObENcSiG2FH-pZSwCaj3Pvy-qvTjnkxxN-3OE6oB8EcX5ekZwCZzAxazbVXctY_hCcaTWG7ugwc_ZyvhsdE7wa3pnTfXYHWXcDDT8FTpYl62aqWsEIUAJSkgmQ9zce0RiDUjBJyJEM9P0ihp1Ab8BD88pEM22-PXfiOesRzp5yOsjzI3kdr5KPsshstneJEGHYae5GZXLUpnVMRY1TCFFLbkPwK6oVkRaVU1RvK9ssO3Q");
    RSAparams.Exponent = Base64UrlDecode("AQAB");
    RSAparams.D = Base64UrlDecode("E4KDwgxy7jFrqeXqKjxPTGOdbEoZ2aWj5qcZhUJcnr9Qh_jg_grkgpHVwEbQifTxsipXTiR3_ygspI4XFoeV-wDVfWqWCVR3_bHChF9PW8Ak1x_dBSS28BMs8PdthI1pDbpqPhmMcF4riHCtNo1M1v8cLdeaiqiXitNVBkaTePsDiucfwOy1rgxwBqAL1CNJhP8oRiYkxD-gfE_EapWuXY9-wF9O-lXPLSTKWgMmmVxSmhUP-Uqk7cJ24UH9C7W7hnSQU4pkfD5XHx3_2WO2GMKKZcqz39wJUrQzrIO7539SYsQ3rEe4aMJyL4U-Ib4_purzVS0DRjzGxK8chT2guQ");
    RSAparams.P = Base64UrlDecode("2TTEToB4AuPIPPpg3yTyBlGb_m-f4r-TxpU96ConV2p696_4QI6jlPWwgcC9Vdma_Da43AGuyLzIptgkzF8nSjV80VwwDKQ1YkFPc6ZqB2isvExuieSP6_jLlB-fCyCLqtTxpPm2VcK16Pqm0s5T0QGH6cQjjm1r2Ww1wuaiQbk");
    RSAparams.Q = Base64UrlDecode("vcpFwkZKZ3hx3FpHgy3ScuuTRSPO2ge8TE8UMJdCrEnpftAeYuVYrJqnxfzKgyl02OijAUi1eozJxj_lM5McxrKZEEAvo6e8wtzl2hnkUh-KWoBJ8ii0VJcu6U5vs4pcv-lYBPFC6fzoGnUw8LNWMxb5ejgYbLUWp10BbfkWGEU");
    RSAparams.DP = Base64UrlDecode("kibhWHk1R6yBlhZbjIrNl9beAkyV5vtFsj_F0ixbIITzjSqI_td71sWjKQvJ2rR7hu5DYTZ4p3XwBeQ2jpYQV-y5uh4v7rGngh-0GHuHqMiUQnejgYGcHgng4iCM4e3aTO7QUlP8jqRfxw6xpfNTjrVbAL8LtdCG21vmqOiLkXE");
    RSAparams.DQ = Base64UrlDecode("qLF9x-zKfaXlLsNgBQ1ZnaQexrnJRqrRh9JSU85fCNy5mmpKWAUbCHB-59CGAId8wMAnAyEpjcBOKNTqWSlNzp84xeUHcyPI-Dt4Yp_Y_dXjGAYntALSJs4qeF2rk55MSpiSD_KSU4DknX_E_G2rFMY7AZOSwi1D8YcNmj5okTE");
    RSAparams.InverseQ = Base64UrlDecode("Mza7JYleki7BvmD3dX5CO2nkD3mBGz4_0P_aoWyHEkWu4p5XWillaRVWyLnQEubLvAduUCr-lhfNmzdUhHecpE438_LQNtKRyOq9zkvjsMOGDmbkKpZ7-aTSshax6KNlYOWdOkadjuLtRExCmwbzu5lgI4NwacxSs5MfjHMrTCo");
    RSA.ImportParameters(RSAparams);
    return RSA;
}

// From the PDF here: https://www.rfc-editor.org/info/rfc7515
// Also see: https://auth0.com/docs/jwks
public static byte[] Base64UrlDecode(string arg)
{
    string s = arg;
    s = s.Replace('-', '+'); // 62nd char of encoding
    s = s.Replace('_', '/'); // 63rd char of encoding
    switch (s.Length % 4) // Pad with trailing '='s
    {
        case 0: break; // No pad chars in this case
        case 2: s += "=="; break; // Two pad chars
        case 3: s += "="; break; // One pad char
        default:
            throw new System.Exception(
        "Illegal base64url string!");
    }
    return Convert.FromBase64String(s); // Standard base64 decoder
}

由于要在SHA-2-256上使用OAEP,因此需要从RSACryptoServiceProvider迁移到RSACng(.NET 4.6+).请注意,除了ctor调用之外,我还消除了有关使用哪种实现的知识.

Because you're using OAEP with SHA-2-256 you need to move from RSACryptoServiceProvider to RSACng (.NET 4.6+). Note that aside from the ctor call, I've eliminated the knowledge of which implementation is being used.

private static RSA GetPrivateKey()
{
    // build the RSAParams as before, then
    RSA rsa = new RSACng();
    rsa.ImportParameters(RSAparams);
    return rsa;
}

// Setup encryption algorithm.
var provider = GetPrivateKey();
...
var decryptedData = provider.Decrypt(encryptedData, RSAEncryptionPadding.OaepSHA256);