0

On the server, I have an PHP script (Laravel) that can generate RSA key pairs, flash them into the session, and return a base64 encoded public key, which will be used in my windows application to encrypt the password, and the key is created by calling the openssl_pkey_new function:

// get RSA key pairs
public function generateRSAKeyPairTest(Request $request)
{
    $t = $this->microtime_float();

    $res = openssl_pkey_new();
    openssl_pkey_export($res, $privkeyraw);
    $d= openssl_pkey_get_details($res);
    $pubkeyraw = $d['key'];
    $pubkey = base64_encode($pubkeyraw);
    $privkey = base64_encode($privkeyraw);
    $request->session()->flash('privkey', $privkey);
    $request->session()->flash('pubkey', $pubkey);

    $tdiff = $this->microtime_float() - $t;
    $keypair = array('pubkey'  => $pubkey,'diff' => $tdiff);

    return response()->json($keypair);
}

// decode the encryted string
public function decryptRSATest(Request $request)
{
    if ($request->session()->has('privkey')) 
    {
        $privkey = $request->session()->get('privkey', 'default value');
        $resPriv = openssl_pkey_get_private(base64_decode($privkey));

        $encrypted_text_base64 = $request->encrypted_text;
        $outval = '-';
        $encrypted_text = base64_decode($encrypted_text_base64);
        openssl_private_decrypt($encrypted_text, $outval, $resPriv);
        return response()->json(
            [
                 'decoded_text_from_clit'=>$outval
            ]
        );
    }
    return response()->json(["error"=>"private key does not exist!"]);
}

then in my windows application (C#), I get the json from server and retrieve the public key

    var data = Convert.FromBase64String(publicKeyStringBase64);
    var publicKeyRaw = Encoding.UTF8.GetString(data);
    var pkStr = publicKeyRaw.Replace("-----END PUBLIC KEY-----", "").Replace("-----BEGIN PUBLIC KEY-----", "");
    var publicKey = Encoding.UTF8.GetBytes(pkStr);

the publicKeyRaw is like following (PKCS#8 format):

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwg6U1EET7OSbLO7UUZh 7p8ODYY4kXUd5S1Z/qexG5IqpNflrdQbpVh+8KWNi83oidAUjWEb050Rl3AuY/E6 7hYlEdUvI9pevmBpjjU1GktzQsDsbva3THHSpTZXPlctnFnuz0b5hVu1nUETmbGF fSbslZet3pbKcK5KGnJpm6v6OQGpvgQjyNWF16HjUD4/x1rAL2aDNOZZED+FNJcC hNmdK1A8nECk1JoTTdiK7r0EXMWxdVjEaSkAsvi7ywKi7ZESWwS1JmRuIJ5ZPiRx Fvur1tgaiomEZ+9oDpk1+bwDenrERYgBxw2L6Rw0CwuyinhwYfIbWkNmy4cAiZVx KwIDAQAB -----END PUBLIC KEY-----

I tried to use RSAParameters, but whether the Modulus is the raw byte array (data) or the array converted from the stripped string (publicKey), the encryptedText encrypted by using RSACryptoServiceProvider's Encrypt method cannot be decrypted by openssl_private_decrypt on the server.

var rsaInfo = new RSAParameters()
{
    Exponent = new byte[] { 1, 0, 1 },
    Modulus = data, // publicKey 
};

var csp = new RSACryptoServiceProvider();
csp.ImportParameters(rsaInfo);
var password = "testing-passsword!";
var hash = csp.Encrypt(Encoding.UTF8.GetBytes(password), false);
var encryptedText = Convert.ToBase64String(hash);

I guess that RSACryptoServiceProvider does not encrypt the password correctly, so what is the correct way to use this key string (publicKeyRaw ) with the RSACryptoServiceProvider ?

[Update]

After doing a lot of searching work on google, the prolem becomes clear (just the same as what @James Reinstate Monica Polk mentions): the key generated by using openssl_pkey_new() is, by default, in PKCS#8 format, while RSACryptoServiceProvider only accepts the key in PKCS#1 v1.5 format. Such incompatibility caused this problem.

so, do you have any solutions for this?

Winfred
  • 19
  • 6

1 Answers1

0

I've got a solution from this post: Correctly create RSACryptoServiceProvider from public key

there are many things we need to take care of when using the RSAPrameters class.

  1. it is recommended to encode the public key by using base64_encode on the server so that it could be passed to the client in a json string. And I use phpseclib instead of the openssl. to genereate the PKCS#1 format public key
  2. In C#,decode the base64 encoded string into a readable public key string, which has a format as follows:
        -----BEGIN RSA PUBLIC KEY-----
        base64 encoded data
         -----END RSA PUBLIC KEY-----
  1. In C#, retrieve the public key data and extract the modulus and public exponent byte arrays. In this step, we should pay a particular attention to Modulus in the RSAParameters class, when we fill in the Modulus, we should skip the fist byte, which is 0x00. I implemented a simple but not complete decoder as follows:
public static RSAParameters GetRSAParametersFromPublicKey(string publicKeyString)
{
            // publicKeyString should have following format:

            // -----BEGIN RSA PUBLIC KEY---- -
            // base64 encoded data
            // -----END RSA PUBLIC KEY---- -

            var pubicKeyContentString = publicKeyString.Replace("-----BEGIN RSA PUBLIC KEY-----", "")
                .Replace("-----END RSA PUBLIC KEY-----", "")
                .Replace("\r", "")
                .Replace("\n", "")
                .Replace(@"\/", "/");
            var publicKeyArray = Convert.FromBase64String(pubicKeyContentString);

            var mask = 0x7F;
            var skipCount = 0;
            var rsaParameters = new RSAParameters();
            for (int i = 0; i < publicKeyArray.Length; i=skipCount)
            {
                var tag = publicKeyArray[i];
                var lengthLength = publicKeyArray[i + 1];
                var length = Convert.ToInt32(lengthLength);
                skipCount += 2;
                if (lengthLength > mask)
                {
                    var lengthBit = lengthLength & mask;
                    var lengthBytes = publicKeyArray.Skip(skipCount).Take(lengthBit).ToArray();
                    skipCount += lengthBit;
                    length = BitConverter.ToInt16(lengthBytes.Reverse().ToArray(),0);
                }
                if (tag == 0x02)
                {
                    // both modulus and public exponent start with 0x02: integer
                    // therefore, 0x02 is the only tag we are interested in
                    var valueBytes = publicKeyArray.Skip(skipCount).Take(length).ToArray();
                    if (valueBytes[0] == 0x00)
                    {
                        // a valid DER has a leading byte 0x00
                        rsaParameters.Modulus = valueBytes.Skip(1).ToArray();
                    }
                    else
                    {
                        rsaParameters.Exponent = valueBytes;
                    }
                    skipCount += length;
                }
            }
            return rsaParameters;
        }

and here is a function that encrypt the input plainText with input base64 encoded public key string

public static string Cryptography(string plainText, string publicKeyStringBase64)
{
    var publicKey = Convert.FromBase64String(publicKeyStringBase64);
    var publicKeyRaw = Encoding.UTF8.GetString(publicKey);
    var rsaInfo = GetRSAParametersFromPublicKey(publicKeyRaw);
    if (rsaInfo.Modulus != null && rsaInfo.Exponent != null)
    {
        var csp = new RSACryptoServiceProvider();
        csp.ImportParameters(rsaInfo);
        var hash = csp.Encrypt(Encoding.Unicode.GetBytes(plainText), false);
        var encryptedText = HttpUtility.UrlEncode(Convert.ToBase64String(hash));

        return encryptedText;
    }
    return "!!error!!";
}
  1. On the server, I'd like to use phpseclib instead to generate a PKCS#1 key pair and perform the decoding (laravel):
    use phpseclib\Crypt\RSA;

    public function decryptRSATest(Request $request)
    {
        if ($request->session()->has('privkey')) 
        {
            $privkeybase64 = $request->session()->get('privkey', 'default value');
            str_replace(['\/', '\n'], ['/', ''], $privkeybase64);
            $privkey = base64_decode($privkeybase64);

            $rsa = new RSA();
            $received = str_replace(['\/', '\n',"\0",'\\'], ['/', '', '',""], $request->encrypted_text);
            $rsa->loadKey($privkey);
            $rsa->setEncryptionMode(RSA::ENCRYPTION_PKCS1);
            $decrypt_text2 = $rsa->decrypt(base64_decode($received));
            $decrypt_text2 = str_replace("\0",'',$decrypt_text2);

            return response()->json(
                [
                     'decoded_text_from_clit' => $decrypt_text2
                ]
            );
        }
        return response()->json(["error"=>"private key does not exist!"]);
    }

Moreover, if we just need the modulus and the exponent instead of a complete public key, we can just pass the modulus and exponent to the client (base64_encoded):

            // PHP - Server end
            $modulus_base64 = base64_encode($rsa->modulus);
            $exponent_base64 = base64_encode($rsa->exponent);

and in the C#, it might be a little bit tough to correctly decode them, here is a code snippet:

        // C# - Client end
        var modulus = Convert.FromBase64String(modulusStringBase64);
        var exponent = Convert.FromBase64String(exponentStringBase64);
        var modulusRaw = Encoding.UTF8.GetString(modulus);
        var exponentRaw = Encoding.UTF8.GetString(exponent);
        var bigIntegerModulus = BigInteger.Parse(modulusRaw);
        var bigIntegerExponent = BigInteger.Parse(exponentRaw);
        var exponentArray = bigIntegerExponent.ToByteArray();
        var modulusArray = bigIntegerModulus.ToByteArray();

and some useful online tools:

base64 decoder

ASN 1 decoder

Json Viewer

Hope this could be helpful for you.

Winfred
  • 19
  • 6
  • `str_replace(['\/', '\n'], ['/', ''], $privkeybase64);` is oddly dangling in your snippet. Did you mean to say `$privkeybase64 = str_replace(['\/', '\n'], ['/', ''], $privkeybase64);`? – mickmackusa Oct 15 '21 at 06:16