1

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.

Kraang Prime
  • 9,981
  • 10
  • 58
  • 124
  • @MaartenBodewes - not asking for writing code for me. But in any case, I am pasting it now -- unfortunately, there is no 'MCVE' for loading a PEM public cert (as can be documented on this site in hundreds of locations), but will do my best to break it down into baby chunks. --- the update should satisfy all your woes. It has been broken into little bite sized nibbles -- however, the solution still fails. Decrypt failure in C#, can be because of literally anything - every piece of code there is a potential point of failure resulting in rsa.Decrypt() barfing. – Kraang Prime Dec 31 '16 at 08:41
  • OK, that's better. What's still missing is a description how the above code is failing. Note that encryption with a private key is completely senseless and very likely *insecure even if the public key is kept secret*. Splitting plaintext in chunks is not a good idea either, look up what hybrid encryption is. – Maarten Bodewes Dec 31 '16 at 09:12
  • @MaartenBodewes - i understand that, but PGP is not viable given that there is no straight c# implementation, and the public vs private, there are articles from security companies on both sides saying encrypt with pub, decrypt with private, and vice versa. Seems no one has a consensus on how to do this, nor a viable cross-language open implementation. (C# <> php <> java <> ios) without use of a 3rd party lib, and even still. Also, I did post an update as to where it is failing, but that doesn't mean it isn't/won't fail elsewhere. I need a solid implementation. – Kraang Prime Dec 31 '16 at 09:21
  • @MaartenBodewes - ... also, some encryption (even if in chunks due to broken limits) is better than none. Alternative is write an encryption method from scratch really -- or fall back to my 3DES cross platform method I wrote for Objective C, Java, PHP, VB.NET, and C# -- really would like to move on from that. – Kraang Prime Dec 31 '16 at 09:24
  • Any security company describing encryption with a private key should be dismantled destroyed or nuked from orbit, with the possible exception that they mean signature generation using the **old** PKCS#1 terms (the OID for RSA signature generation is still **RSA encryption**, which is incorrect, it just uses modular exponentiation just like encryption). – Maarten Bodewes Dec 31 '16 at 09:24
  • Some encryption such as often posted here on SO is **worse** than no encryption because it only provides a **false** sense of security, where there is none. PS please provide error message! – Maarten Bodewes Dec 31 '16 at 09:25
  • @MaartenBodewes - i feel that way in regards to 1/2 the bloated and/or broken methods/libs out in the wild. It is really just another form of lying and the disinformation is doing a disservice to encryption. – Kraang Prime Dec 31 '16 at 09:26
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/131955/discussion-between-maarten-bodewes-and-kraang-prime). – Maarten Bodewes Dec 31 '16 at 09:27
  • I have the decryption / encryption working now, which was a huge hurdle (and updated the question using the proper order for encryption/decryption - decrypt with private - encrypt with public -. Still missing is what this problem was primarily focused on which is decrypting in chunks. The chunk size of the encrypted data is larger than the unencrypted string. – Kraang Prime Dec 31 '16 at 12:26
  • Thats normal for RSA v1.5 padding. Output ciphertext is size of modulus (i.e. key size) and input is 11 bytes less. – Maarten Bodewes Dec 31 '16 at 12:56
  • How can I account for that, so that the chunk reading is reliable ? Would you be so kind to demonstrate (as that would resolve this problem). Essentially looping through the base64 decoded bytes in increments of X that equates to php's 100 byte chunks. – Kraang Prime Dec 31 '16 at 13:10
  • You can first base 64 decode, then split up according to the modulus / key size, decrypt and put it in a memory stream. Then finally you can use that stream to retrieve the plaintext (as bytes). Then you need to convert back to text (if it was text) and you're done. – Maarten Bodewes Dec 31 '16 at 13:26
  • @MaartenBodewes - clearly it is the size that i am having the trouble with -- take a loot at the bottom most code sample for what I have tried previously. Need to replace 100 with whatever from wherever, however. `rsa.KeySize` is 4096. – Kraang Prime Dec 31 '16 at 13:29
  • Mmm, I don't see the discussion [in chat](http://chat.stackoverflow.com/rooms/131969/discussion-between-maarten-bodewes-and-kraang-prime) turn up here. Can you have a look? – Maarten Bodewes Dec 31 '16 at 13:37

1 Answers1

2

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.
Community
  • 1
  • 1
Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
  • A couple of things to note -- the question changed so GetPublicKey doesn't apply from c# side. Also, that stackoverflow code doesn't create anything :) . The chunking is done at 100 bytes per chunk, however to decrypt as stated the `KeySize / 8` is the chunk size to decode. Now if only there was a way to use pub/priv keys to encrypt larger data without the need for chunking. – Kraang Prime Dec 31 '16 at 14:31
  • Generally you encrypt an AES key and use that instead. That's hybrid cryptography, as indicated in my second comment under the question (now the first). – Maarten Bodewes Dec 31 '16 at 14:35
  • I added the resulting source code which made this work for me to your solution. – Kraang Prime Dec 31 '16 at 14:43