How can I decrypt data in chunks in c# using a private key after encrypting in php using a public key?

2.8k views Asked by At

How do I decrypt the output of this code using the private key (pem format) in C# ?

$output = json_encode(array('see'=>'me'));

define('CIPHER_BLOCK_SIZE', 100);

$encrypted = '';
$key = file_get_contents('public.txt');

$chunks = str_split($output, CIPHER_BLOCK_SIZE);
foreach($chunks as $chunk)
{
  $chunkEncrypted = '';
  $valid = openssl_public_encrypt($chunk, $chunkEncrypted, $key, OPENSSL_PKCS1_PADDING);

  if($valid === false){
      $encrypted = '';
      break; //also you can return and error. If too big this will be false
  } else {
      $encrypted .= $chunkEncrypted;
  }
}
$output = base64_encode($encrypted); //encoding the whole binary String as MIME base 64

echo $output;

Click here for a large json sample ready formatted to replace the following line in the above sample, to test chunking, as the $output json above is too small for chunking to take effect.

$output = json_encode(array('see'=>'me'));

Explanation of what the code above does

The above code is a modification of this solution which breaks the data into smaller chunks (100 bytes per chunk) and encrypts them using a public key in pem format.

Objective

I am looking at encrypting larger than a few bytes for more secure transit of data, and found that encrypting/decrypting using certificates is the best route to go.

The intent is to encrypt data in php (using the private key) which would then be received in an application written in C# and decrypted (using the public key).

C# - The road so far


Following is my attempt at decrypting in c# :

Usage :

// location of private certificate
string key = @"C:\path\to\private.txt";

// output from php script (encrypted)
string encrypted = "Bdm4s7aw.....Pvlzg=";

// decrypt and store decrypted string
string decrypted = crypt.decrypt( encrypted, key );

Class :

public static string decrypt(string encrypted, string privateKey) {
    try {
        RSACryptoServiceProvider rsa = DecodePrivateKeyInfo( DecodePkcs8PrivateKey( File.ReadAllText( privateKey ) ) );
        return Encoding.UTF8.GetString( rsa.Decrypt( Convert.FromBase64String( encrypted ), false ) );
    } catch (CryptographicException ce) {
        return ce.Message;
    } catch (FormatException fe) {
        return fe.Message;
    } catch (IOException ie) {
        return ie.Message;
    } catch (Exception e) {
        return e.Message;
    }
}

The other methods this depends on (harvested from opensslkey.cs )

//--------   Get the binary PKCS #8 PRIVATE key   --------
private static byte[] DecodePkcs8PrivateKey( string instr ) {
    const string pemp8header = "-----BEGIN PRIVATE KEY-----";
    const string pemp8footer = "-----END PRIVATE KEY-----";
    string pemstr = instr.Trim();
    byte[] binkey;
    if ( !pemstr.StartsWith( pemp8header ) || !pemstr.EndsWith( pemp8footer ) )
        return null;
    StringBuilder sb = new StringBuilder( pemstr );
    sb.Replace( pemp8header, "" );  //remove headers/footers, if present
    sb.Replace( pemp8footer, "" );

    string pubstr = sb.ToString().Trim();   //get string after removing leading/trailing whitespace

    try {
        binkey = Convert.FromBase64String( pubstr );
    } catch ( FormatException ) {        //if can't b64 decode, data is not valid
        return null;
    }
    return binkey;
}

//------- Parses binary asn.1 PKCS #8 PrivateKeyInfo; returns RSACryptoServiceProvider ---
private static RSACryptoServiceProvider DecodePrivateKeyInfo( byte[] pkcs8 ) {
    // encoded OID sequence for  PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
    // this byte[] includes the sequence byte and terminal encoded null
    byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
    byte[] seq = new byte[15];
    // ---------  Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob  ------
    MemoryStream mem = new MemoryStream( pkcs8 );
    int lenstream = (int)mem.Length;
    BinaryReader binr = new BinaryReader( mem );    //wrap Memory Stream with BinaryReader for easy reading
    byte bt = 0;
    ushort twobytes = 0;

    try {

        twobytes = binr.ReadUInt16();
        if ( twobytes == 0x8130 )   //data read as little endian order (actual data order for Sequence is 30 81)
            binr.ReadByte();    //advance 1 byte
        else if ( twobytes == 0x8230 )
            binr.ReadInt16();   //advance 2 bytes
        else
            return null;


        bt = binr.ReadByte();
        if ( bt != 0x02 )
            return null;

        twobytes = binr.ReadUInt16();

        if ( twobytes != 0x0001 )
            return null;

        seq = binr.ReadBytes( 15 );     //read the Sequence OID
        if ( !CompareBytearrays( seq, SeqOID ) )    //make sure Sequence for OID is correct
            return null;

        bt = binr.ReadByte();
        if ( bt != 0x04 )   //expect an Octet string
            return null;

        bt = binr.ReadByte();       //read next byte, or next 2 bytes is  0x81 or 0x82; otherwise bt is the byte count
        if ( bt == 0x81 )
            binr.ReadByte();
        else
            if ( bt == 0x82 )
            binr.ReadUInt16();
        //------ at this stage, the remaining sequence should be the RSA private key

        byte[] rsaprivkey = binr.ReadBytes( (int)( lenstream - mem.Position ) );
        RSACryptoServiceProvider rsacsp = DecodeRSAPrivateKey( rsaprivkey );
        return rsacsp;
    } catch ( Exception ) {
        return null;
    } finally { binr.Close(); }

}

//------- Parses binary ans.1 RSA private key; returns RSACryptoServiceProvider  ---
private static RSACryptoServiceProvider DecodeRSAPrivateKey( byte[] privkey ) {
    byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;

    // ---------  Set up stream to decode the asn.1 encoded RSA private key  ------
    MemoryStream mem = new MemoryStream( privkey );
    BinaryReader binr = new BinaryReader( mem );    //wrap Memory Stream with BinaryReader for easy reading
    byte bt = 0;
    ushort twobytes = 0;
    int elems = 0;
    try {
        twobytes = binr.ReadUInt16();
        if ( twobytes == 0x8130 )   //data read as little endian order (actual data order for Sequence is 30 81)
            binr.ReadByte();    //advance 1 byte
        else if ( twobytes == 0x8230 )
            binr.ReadInt16();   //advance 2 bytes
        else
            return null;

        twobytes = binr.ReadUInt16();
        if ( twobytes != 0x0102 )   //version number
            return null;
        bt = binr.ReadByte();
        if ( bt != 0x00 )
            return null;


        //------  all private key components are Integer sequences ----
        elems = GetIntegerSize( binr );
        MODULUS = binr.ReadBytes( elems );

        elems = GetIntegerSize( binr );
        E = binr.ReadBytes( elems );

        elems = GetIntegerSize( binr );
        D = binr.ReadBytes( elems );

        elems = GetIntegerSize( binr );
        P = binr.ReadBytes( elems );

        elems = GetIntegerSize( binr );
        Q = binr.ReadBytes( elems );

        elems = GetIntegerSize( binr );
        DP = binr.ReadBytes( elems );

        elems = GetIntegerSize( binr );
        DQ = binr.ReadBytes( elems );

        elems = GetIntegerSize( binr );
        IQ = binr.ReadBytes( elems );

        // ------- create RSACryptoServiceProvider instance and initialize with public key -----
        RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
        RSAParameters RSAparams = new RSAParameters();
        RSAparams.Modulus = MODULUS;
        RSAparams.Exponent = E;
        RSAparams.D = D;
        RSAparams.P = P;
        RSAparams.Q = Q;
        RSAparams.DP = DP;
        RSAparams.DQ = DQ;
        RSAparams.InverseQ = IQ;
        RSA.ImportParameters( RSAparams );
        return RSA;
    } catch ( Exception ) {
        return null;
    } finally { binr.Close(); }
}

private static int GetIntegerSize( BinaryReader binr ) {
    byte bt = 0;
    byte lowbyte = 0x00;
    byte highbyte = 0x00;
    int count = 0;
    bt = binr.ReadByte();
    if ( bt != 0x02 )       //expect integer
        return 0;
    bt = binr.ReadByte();

    if ( bt == 0x81 )
        count = binr.ReadByte();    // data size in next byte
    else
    if ( bt == 0x82 ) {
        highbyte = binr.ReadByte(); // data size in next 2 bytes
        lowbyte = binr.ReadByte();
        byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
        count = BitConverter.ToInt32( modint, 0 );
    } else {
        count = bt;     // we already have the data size
    }



    while ( binr.ReadByte() == 0x00 ) { //remove high order zeros in data
        count -= 1;
    }
    binr.BaseStream.Seek( -1, SeekOrigin.Current );     //last ReadByte wasn't a removed zero, so back up a byte
    return count;
}

private static bool CompareBytearrays( byte[] a, byte[] b ) {
    if ( a.Length != b.Length )
        return false;
    int i = 0;
    foreach ( byte c in a ) {
        if ( c != b[i] )
            return false;
        i++;
    }
    return true;
}

This is all functional now, however it still doesn't incorporate chunking in the decryption process.

What must I do to read these blocks in, as larger files will definitely be larger than the original unencrypted data.

My previous attempt was to try something like the following code, but this seems flawed as it is always padding 100 bytes (even when the total bytes are less), and the base64 decoding of the json_encode(array('see'=>'me')) using my current public key for encrypting ends up being 512 bytes.

    byte[] buffer = new byte[100]; // the number of bytes to decrypt at a time
    int bytesReadTotal = 0;
    int bytesRead = 0;
    string decrypted = "";
    byte[] decryptedBytes;
    using ( Stream stream = new MemoryStream( data ) ) {
        while ( ( bytesRead = await stream.ReadAsync( buffer, bytesReadTotal, 100 ) ) > 0 ) {
            decryptedBytes = rsa.Decrypt( buffer, false );
            bytesReadTotal = bytesReadTotal + bytesRead;
            decrypted = decrypted + Encoding.UTF8.GetString( decryptedBytes );
        }
    }

    return decrypted;

For your convenience, I put up a php script to generate a public and private key to test with on tehplayground.com.

1

There are 1 answers

3
Maarten Bodewes On BEST ANSWER

After an extensive chat with the author of the question it seems that there are two (main) issues in the code that stopped it from working:

  1. The public key wasn't read as the code from this stackoverflow solution actually doesn't create a binary public key but a certificate. For this the X509Certificate constructor can be used followed by GetPublicKey. The method in the stackoverflow solution should have been named differently. This was later changed to a private key (as decryption using a public key doesn't provide confidentiality).

  2. The encrypted chunks were thought to be 100 bytes in size, while the key size was 4096 bits (512 bytes). However RSA (as specified in PKCS#1 v2.1 for PKCS#1 v1.5 padding) always encrypts to exactly the RSA key size (modulus size) in bytes. So the input for decryption should be chunks of 512 bytes as well. The output however will be 100 bytes if that was encrypted (in the PHP code).

To make this work, a small modification was necessary to loop though the base64 decoded bytes of the encrypted data in chunks calculated based on KeySize / 8 (where 8 is how many bits in a byte as KeySize is an int value representing how many bytes each block is).

public static async Task<string> decrypt(string encrypted, string privateKey) {
        // read private certificate into RSACryptoServiceProvider from file
        RSACryptoServiceProvider rsa = DecodePrivateKeyInfo( DecodePkcs8PrivateKey( File.ReadAllText( privateKey ) ) );

        // decode base64 to bytes
        byte[] encryptedBytes = Convert.FromBase64String( encrypted );
        int bufferSize = (int)(rsa.KeySize / 8);

        // initialize byte buffer based on certificate block size
        byte[] buffer = new byte[bufferSize]; // the number of bytes to decrypt at a time
        int bytesReadTotal = 0;    int bytesRead = 0;
        string decrypted = "";     byte[] decryptedBytes;

        // convert byte array to stream
        using ( Stream stream = new MemoryStream( encryptedBytes ) ) {

            // loop through stream for each block of 'bufferSize'
            while ( ( bytesRead = await stream.ReadAsync( buffer, bytesReadTotal, bufferSize ) ) > 0 ) {

                // decrypt this chunk
                decryptedBytes = rsa.Decrypt( buffer, false );

                // account for bytes read & decrypted
                bytesReadTotal = bytesReadTotal + bytesRead;

                // append decrypted data as string for return
                decrypted = decrypted + Encoding.UTF8.GetString( decryptedBytes );
            }
        }

        return decrypted;
}

Security notes:

  • PKCS#1 v1.5 padding is vulnerable to padding oracle attacks, better make sure that you don't allow those, especially in transport protocols (or use the newer OAEP padding instead);
  • trust your public key before using it, or man-in-the-middle attacks may apply.