A.Grandt
A.Grandt

Reputation: 2252

C# DECRYPT a file using the RSA DER format Public key

I've seen a lot of questions, none really provide good answers, or if they try, they assume the one asking the question isn't asking what he actually IS asking. And just for the record, I am NOT asking about signing, encryption or PEM files!

In my case, I'll be receiving a small file of encrypted data. It has been encrypted using the private key of an RSA key pair.

The public key I have available is a DER file in the ASN.1 format, it is not a certificate as such, and it is not signed.

In Java, this is easy, in c# it is a nightmare from what I can tell. X509Certificate2 claims "Object not found", when I try to parse it the byte array of the key.

The keys were generated with the following openSSL commands:

>openssl genrsa -out private_key.pem 2048

Generating RSA private key, 2048 bit long modulus
...........+++
....................................................................................................
...........................+++
e is 65537 (0x10001)

>openssl pkcs8 -topk8 -inform PEM -outform DER -in private_key.pem -out private_key.der -nocrypt

>openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der
writing RSA key

Edit: The code I'm trying:

private byte[] ReadBytesFromStream(string streamName)
{
    using (Stream stream = this.GetType().Assembly.GetManifestResourceStream(streamName))
    {
        byte[] result = new byte[stream.Length];
        stream.Read(result, 0, (int) stream.Length);
        return result;
    }
}

public void LoadPublicKey()
{
    byte[] key = ReadBytesFromStream("my.namespace.Resources.public_key.der");
    X509Certificate2 myCertificate;
    try
    {
        myCertificate = new X509Certificate2(key);
    }
    catch (Exception e)
    {
        throw new CryptographicException(String.Format("Unable to open key file. {0}", e));
    } 
}

The ReadBytesFromStream method does return the correct byte stream, the public_key.der file is an embedded resource.

Upvotes: 3

Views: 7122

Answers (1)

A.Grandt
A.Grandt

Reputation: 2252

Just to polish this one off. The code below is still a bit messy though, but it does seem to be what I could get to work.

The reason we can't use public keys to decrypt a message in Windows is explained here: http://www.derkeiler.com/Newsgroups/microsoft.public.dotnet.security/2004-05/0270.html

Next, you need the public key modulus and exponent in the c# code, somewhere, be it in a file or embedded is really not the matter here. I do use Base64 to wrap up the binary keys though. I gave up on generating a key that Windows would actually import. Besides, for what I need, the public key will be an embedded resource.

The last problem I had were that the modulus generated from the key weren't working on Windows (c#). This StackOverflow post did hold the key to solve that one, the modulus in Windows can not contain leading zeroes. Android in-app billing Verification of Receipt in Dot Net(C#)

The modulus generated from Java didn't work on c#. Use the stripLeadingZeros code from that post to create a working Public key modulus:

  private static byte[] stripLeadingZeros(byte[] a) {
    int lastZero = -1;
    for (int i = 0; i < a.length; i++) {
      if (a[i] == 0) {
        lastZero = i;
      }
      else {
        break;
      }
    }
    lastZero++;
    byte[] result = new byte[a.length - lastZero];
    System.arraycopy(a, lastZero, result, 0, result.length);
    return result;
  }

For starters, this is how I generated the key files:

$ openssl genrsa -out private_key.pem 2048

$ openssl pkcs8 -topk8 -inform PEM -outform DER -in private_key.pem -out private_key.der -nocrypt

$ openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der

The Java Code:

package com.example.signing;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import jcifs.util.Base64;

public class SigningExample {
    private static String PRIVATE_KEY_FILE = "private_key.der";
    private static String PUBLIC_KEY_FILE  = "public_key.der";

    /**
     * @param args
     */
    public static void main(String[] args) {
        SigningExample.sign();
    }

    private static void sign() {
        try {
            String text = new String(SigningExample.loadFromFile("message.xml"));
            String message = Base64.encode(text.getBytes());

            RSAPrivateKey privateKey = PrivateRSAKeyReader.getKeyFile(SigningExample.PRIVATE_KEY_FILE);
            RSAPublicKey publicKey = PublicRSAKeyReader.getKeyFile(SigningExample.PUBLIC_KEY_FILE);

            byte[] modulusBytes = publicKey.getModulus().toByteArray();
            byte[] exponentBytes = publicKey.getPublicExponent().toByteArray();

            modulusBytes = SigningExample.stripLeadingZeros(modulusBytes);

            String modulusB64 = Base64.encode(modulusBytes);
            String exponentB64 = Base64.encode(exponentBytes);

            System.out.println("Copy these two into your c# code:");
            System.out.println("DotNet Modulus : " + modulusB64);
            System.out.println("DotNet Exponent: " + exponentB64);

            // Testing
            byte[] signature = SigningExample.sign(message.getBytes(), privateKey, "SHA1withRSA");

            String signedMessage = message + "\n" + Base64.encode(signature);

            SigningExample.saveToBase64File("message.signed", signedMessage.getBytes());

            System.out.println("\nMessage  :\n" + signedMessage);

            String[] newkey = new String(SigningExample.loadFromBase64File("message.signed")).split("\\n");
            System.out.println("Verified : " + SigningExample.verify(newkey[0].getBytes(), publicKey, "SHA1withRSA", Base64.decode(newkey[1])));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static byte[] stripLeadingZeros(byte[] a) {
        int lastZero = -1;
        for (int i = 0; i < a.length; i++) {
            if (a[i] == 0) {
                lastZero = i;
            } else {
                break;
            }
        }
        lastZero++;
        byte[] result = new byte[a.length - lastZero];
        System.arraycopy(a, lastZero, result, 0, result.length);
        return result;
    }

    private static byte[] sign(byte[] data, PrivateKey prvKey,
            String sigAlg) throws Exception {
        Signature sig = Signature.getInstance(sigAlg);
        sig.initSign(prvKey);
        sig.update(data, 0, data.length);
        return sig.sign();
    }

    private static boolean verify(byte[] data, PublicKey pubKey,
            String sigAlg, byte[] sigbytes) throws Exception {
        Signature sig = Signature.getInstance(sigAlg);
        sig.initVerify(pubKey);
        sig.update(data, 0, data.length);
        return sig.verify(sigbytes);
    }

    public static void saveToBase64File(String fileName, byte[] data) throws IOException {
        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(fileName));

        try {
            String b64 = Base64.encode(data);
            int lineLength = 64;
            int idx = 0;
            int len = b64.length();

            while (len - idx >= lineLength) {
                out.write(b64.substring(idx, idx + lineLength).getBytes());
                out.write('\n');
                idx += lineLength;
            }
            out.write(b64.substring(idx, len).getBytes());
        } catch (Exception e) {
            throw new IOException("Unexpected error", e);
        } finally {
            out.close();
        }
    }

    public static byte[] loadFromBase64File(String fileName) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(fileName));

        try {
            StringBuffer sb = new StringBuffer();

            String buffer;

            while ((buffer = br.readLine()) != null) {
                sb.append(buffer);
            }

            return Base64.decode(sb.toString());
        } catch (Exception e) {
            throw new IOException("Unexpected error", e);
        } finally {
            br.close();
        }
    }

    public static void saveToFile(String fileName, byte[] data) throws IOException {
        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(fileName));

        try {
            out.write(data);
        } catch (Exception e) {
            throw new IOException("Unexpected error", e);
        } finally {
            out.close();
        }
    }

    public static byte[] loadFromFile(String fileName) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(fileName));

        try {
            StringBuffer sb = new StringBuffer();

            String buffer;

            while ((buffer = br.readLine()) != null) {
                sb.append(buffer);
            }

            return sb.toString().getBytes();
        } catch (Exception e) {
            throw new IOException("Unexpected error", e);
        } finally {
            br.close();
        }
    }
}

class PrivateRSAKeyReader {

    public static RSAPrivateKey getKeyFile(String filename) throws Exception {

        File f = new File(filename);
        FileInputStream fis = new FileInputStream(f);
        DataInputStream dis = new DataInputStream(fis);
        byte[] keyBytes = new byte[(int) f.length()];
        dis.readFully(keyBytes);
        dis.close();

        return PrivateRSAKeyReader.generateKey(keyBytes);
    }

    public static RSAPrivateKey getKey(String base64) throws Exception {
        byte[] keyBytes = Base64.decode(base64);

        return PrivateRSAKeyReader.generateKey(keyBytes);
    }

    private static RSAPrivateKey generateKey(byte[] keyBytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        return (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(spec);
    }
}

class PublicRSAKeyReader {

    public static RSAPublicKey getKeyFile(String filename) throws Exception {

        File f = new File(filename);
        FileInputStream fis = new FileInputStream(f);
        DataInputStream dis = new DataInputStream(fis);
        byte[] keyBytes = new byte[(int) f.length()];
        dis.readFully(keyBytes);
        dis.close();

        return PublicRSAKeyReader.generateKey(keyBytes);
    }

    public static RSAPublicKey getKey(String base64) throws Exception {
        byte[] keyBytes = Base64.decode(base64);

        return PublicRSAKeyReader.generateKey(keyBytes);
    }

    private static RSAPublicKey generateKey(byte[] keyBytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(spec);
    }
}

In c# this code fragment should do the tricks:

String signedMessage = ""; // load Base64 coded the message generated in Java here.

byte[] message = Convert.FromBase64String(signedMessage);
String messageString = Encoding.ASCII.GetString(message);

String[] lines = Regex.Split(messageString, "\n");

byte[] content = Convert.FromBase64String(lines[0]); // first line of the message were the content
byte[] signature = Convert.FromBase64String(lines[1]); // second line were the signature

RSACryptoServiceProvider rsaObj = new RSACryptoServiceProvider(2048);

//Create a new instance of the RSAParameters structure.
RSAParameters rsaPars = new RSAParameters();

rsaPars.Modulus = Convert.FromBase64String("insert your modulus revealed in the Java example here");
rsaPars.Exponent = Convert.FromBase64String("AQAB"); // Exponent. Should always be this.

// Import key parameters into RSA.
rsaObj.ImportParameters(rsaPars);

// verify the message
Console.WriteLine(rsaObj.VerifyData(Encoding.ASCII.GetBytes(lines[0]), "SHA1", signature));

The code is using :

using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

I freely admit that packing the final message into another layer of Base64 is a little overkill and should be changed.

Upvotes: 1

Related Questions