2

I am trying to implement my own RSA encryption engine. Given these RSA algorithm values:

p = 61. // A prime number.
q = 53. // Also a prime number.
n = 3233. // p * q.
totient = 3120. // (p - 1) * (q - 1)
e = 991. // Co-prime to the totient (co-prime to 3120).
d = 1231. // d * e = 1219921, which is equal to the relation where 1 + k * totient = 1219921 when k = 391.

I am trying to write a method to encrypt each byte in a string and return back an encrypted string:

public string Encrypt(string m, Encoding encoding)
{
    byte[] bytes = encoding.GetBytes(m);
    for (int i = 0; i < bytes.Length; i++)
    {
        bytes[i] = (byte)BigInteger.ModPow(bytes[i], e, n);
    }
    string encryptedString = encoding.GetString(bytes);
    Console.WriteLine("Encrypted {0} as {1}.", m, encryptedString);
    return encryptedString;
}

The obvious issue here is that BigInteger.ModPow(bytes[i], e, n) may be too large to fit into a byte-space; it could result in values over 8 bits in size. How do you get around this issue while still being able to decrypt an encrypted string of bytes back into a regular string?

Update: Even encrypting from byte[] to byte[], you reach a case where encrypting that byte using the RSA algorithm goes beyond the size limit of a byte:

public byte[] Encrypt(string m, Encoding encoding)
{
    byte[] bytes = encoding.GetBytes(m);
    for (int i = 0; i < bytes.Length; i++)
    {
        bytes[i] = (byte)BigInteger.ModPow(bytes[i], e, n);
    }
    return bytes;
}

Update: My issue is that encryption would cause a greater number of bytes than the initial input string had:

public byte[] Encrypt(string m, Encoding encoding)
{
    byte[] bytes = encoding.GetBytes(m);
    byte[] returnBytes = new byte[0];
    for (int i = 0; i < bytes.Length; i++)
    {
        byte[] result = BigInteger.ModPow(bytes[i], (BigInteger)e, n).ToByteArray();
        int preSize = returnBytes.Length;
        Array.Resize(ref returnBytes, returnBytes.Length + result.Length);
        result.CopyTo(returnBytes, preSize);
    }
    return returnBytes;
}

public string Decrypt(byte[] c, Encoding encoding)
{
    byte[] returnBytes = new byte[0];
    for (int i = 0; i < c.Length; i++)
    {
        byte[] result = BigInteger.ModPow(c[i], d, n).ToByteArray();
        int preSize = returnBytes.Length;
        Array.Resize(ref returnBytes, returnBytes.Length + result.Length);
        result.CopyTo(returnBytes, preSize);
    }
    string decryptedString = encoding.GetString(returnBytes);
    return decryptedString;
}

If you ran this code like this:

byte[] encryptedBytes = engine.Encrypt("Hello, world.", Encoding.UTF8);
Console.WriteLine(engine.Decrypt(encryptedBytes, Encoding.UTF8));

The output would be this:

?♥D
?♥→☻►♦→☻►♦oD♦8? ?♠oj?♠→☻►♦;♂?♠♂♠?♠

Obviously, the output is not the original string because I can't just try decrypting each byte at a time, since sometimes two or more bytes of the cypher-text represent the value of one integer that I need to decrypt back to one byte of the original string...so I want to know what the standard mechanism for handling this is.

Alexandru
  • 12,264
  • 17
  • 113
  • 208
  • 5
    First get your encryption working on `byte[]` to `byte[]` mode. *Then* worry about text conversions. They're entirely separate - and you should *not* use `Encoding.GetString` on data which isn't actually encoded text. (I assume you're only implementing this for academic interest? If you're trying to do so for production, then stop right now and use an implementation created by experts.) – Jon Skeet Jun 17 '14 at 15:12
  • @JonSkeet Yeah, you're right...Encoding.GetString might result in losses because that encoding might not support a certain byte-value. – Alexandru Jun 17 '14 at 15:17
  • @JonSkeet Even in byte[] to byte[] mode, you will reach a case where encrypting one byte using the RSA encryption algorithm goes beyond the size limit of a byte. I will update this into the question. – Alexandru Jun 17 '14 at 15:34
  • BigInteger.ToByteArray() would help, but it would increase the number of elements in the returned byte array in some cases. It would require a mechanism to determine whether the byte-string has extra elements or not. So what you're telling me is that, no matter what implementation created by experts I use, they will never be universally compatible with one another? In other words, I can encrypt using one expert's algorithm, but I would not necessarily be able to decrypt that cypher-text using another expert's algorithm? – Alexandru Jun 17 '14 at 15:44
  • I never said anything of the sort. Correct implementations should be compatible. I don't see what the has to do with anything else in your question. But again, I would caution you against using amateur crypto implementations for anything even slightly sensitive. Getting this kind of thing right is hard. – Jon Skeet Jun 17 '14 at 15:46
  • @JonSkeet I don't think that's true. Especially not if two different libraries use two different padding schemes: https://www.iacr.org/archive/crypto2002/24420226/24420226.pdf I know its hard to get it right, but that's why I came here. I want to get input from people who are experts in cryptography as to how they circumvent this issue. – Alexandru Jun 17 '14 at 15:56
  • That's why decent implementations will allow you to specify the padding. Yes, you need to configure the implementation correctly - but the point remains that two implementations of the same algorithm *should* be compatible. If they weren't, any client/server cryptography on the internet would be fundamentally broken - we'd all have to be running the exact same code. That's clearly not the case - how do you explain that? (To be clear, many experts can implement the same algorithm. Yes, you certainly need to be using the same algorithm to encrypt and decrypt, but different implementaions.) – Jon Skeet Jun 17 '14 at 16:12
  • And it's still not clear what "issue" you're talking about that would need circumventing. If it's simply that the number of bytes of input isn't always the same as the number of bytes of output, I don't see that as an issue - and different algorithms have different approaches to that (e.g. block cyphers vs stream cyphers). – Jon Skeet Jun 17 '14 at 16:14
  • @JonSkeet I updated the question again to explain what my issue is. – Alexandru Jun 17 '14 at 16:41
  • Well that shows that your implementation is broken - which I'm not surprised about, to be honest. You seem to be assuming that the return value of `BigInteger.ToByteArray()` is suitable for this, iterating byte-by-byte over the original plaintext when encrypting and the ciphertext when decrypting. There's a lot more to it than that. Given your approach, you might want to consider encrypting to a `BigInteger[]` and see if you can decrypt *that*. Just as a stepping stone, at least. – Jon Skeet Jun 17 '14 at 16:44
  • Hopefully my answer will help you get one step along the way... – Jon Skeet Jun 17 '14 at 16:52
  • @JonSkeet I woke up this morning, RSA in my head, and had a sudden vision. I read up and found a better article on RSA: http://en.wikipedia.org/wiki/RSA_(cryptosystem) This article says (under the encryption category): "He first turns M into an integer m, such that 0 ≤ m < n by using an agreed-upon reversible protocol known as a padding scheme." The most important thing here is that the encrypted integer can be reversible only if m is less than the value of n, so I need to generate values large enough for p, q, and n to be able to convert an entire string to a BigInteger... – Alexandru Jun 18 '14 at 12:44
  • ...and then encrypt it, while ensuring that the integer in question is less than n. Otherwise, it won't be decypherable. That's the solution here! I will work on an answer in a bit, but I need to generate larger values (can take awhile). – Alexandru Jun 18 '14 at 12:45
  • @JonSkeet I've updated my answer to reflect this. – Alexandru Jun 18 '14 at 13:18
  • I'm still concerned that you're potentially going to actually *use* your own crypto implementation. I think I've said all I can here: it's a really bad idea. I've answered the question as clearly as I can... I think I'm done now. – Jon Skeet Jun 18 '14 at 13:21
  • @JonSkeet Aside from using or not using it, thanks for your help. It was a very good and also interesting exercise to find a solution for the non-padded RSA encryption scheme. Other people will also benefit from this solution because it will teach them how to encrypt on a very basic level and also how the innards of RSA work, and that's priceless. For everything else, there's MasterCard...for third party libraries. – Alexandru Jun 18 '14 at 13:30
  • 1) You must use OAEP padding with RSA. PKCS#1v1.5 padding is only secure with extremely careful use and no padding, like in your example is utterly insecure. Since this padding costs over 40 bytes, your short keys plain don't work. 2) RSA keys should have more than 1000 bits. The recommended size is 2048 bits and 512 bit keys can be broken by hobbyists. 3) Don't encrypt long strings with RSA. Generate a short (256 bits or so) random AES key, encrypt the message with AES and that key with RSA. That way you don't run into those "string is longer than n" problems. – CodesInChaos Jun 18 '14 at 18:39
  • @CodesInChaos "Generate a short (256 bits or so) random AES key, encrypt the message with AES and that key with RSA. That way you don't run into those "string is longer than n" problems." - Is that how it is normally done as a standard, by using AES first and then RSA? Because like Jon said, he would expect all implementations of cryptographic libraries that follow standards to be compatible with one another. – Alexandru Jun 18 '14 at 18:56
  • @Alexandru The RSA standard has simply limited plaintext length, longer plaintexts cause an error. The limit is something like ModulusSize - 42 bytes (depending on padding). `RSACryptoServiceProvider` works like that. There are some higher level specifications which include hybrid encryption, like OpenPG or CMS, but they're considerably more complex. – CodesInChaos Jun 18 '14 at 19:36
  • Right, so most standard implementations just pad using OAEP and then encrypt with RSA, correct? – Alexandru Jun 18 '14 at 23:10

3 Answers3

2

Your basic code for encrypting and decrypting each byte - the call to ModPow - is working, but you're going about the "splitting the message up and encrypting each piece" inappropriately.

To show that the ModPow part - i.e. the maths - is fine, here's code based on yours, which encrypts a string to a BigInteger[] and back:

using System;
using System.Linq;
using System.Numerics;
using System.Text;

class Test
{
    const int p = 61;
    const int q = 53;
    const int n = 3233;
    const int totient = 3120;
    const int e = 991;
    const int d = 1231;

    static void Main()
    {
        var encrypted = Encrypt("Hello, world.", Encoding.UTF8);
        var decrypted = Decrypt(encrypted, Encoding.UTF8);
        Console.WriteLine(decrypted);
    }

    static BigInteger[] Encrypt(string text, Encoding encoding)
    {
        byte[] bytes = encoding.GetBytes(text);
        return bytes.Select(b => BigInteger.ModPow(b, (BigInteger)e, n))
                    .ToArray();
    }

    static string Decrypt(BigInteger[] encrypted, Encoding encoding)
    {
        byte[] bytes = encrypted.Select(bi => (byte) BigInteger.ModPow(bi, d, n))
                                .ToArray();
        return encoding.GetString(bytes);
    }
}

Next you need to read more about how a byte[] is encrypted into another byte[] using RSA, including all the different padding schemes etc. There's a lot more to it than just calling ModPow on each byte.

But to reiterate, you should not be doing this to end up with a production RSA implementation. The chances of you doing that without any security flaws are very slim indeed. It's fine to do this for academic interest, to learn more about the principles of cryptography, but leave the real implementations to experts. (I'm far from an expert in this field - there's no way I'd start implementing my own encryption...)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Right, so in your example you encrypt to an array of the BigInteger data-type and then decrypt back from it to the original string. However, this would not have native cross-platform support, because I would not be able to use the BigInteger datatype in other platforms natively, so I could not create an array of BigInteger to send across a network layer for decryption from a non-C# device, like an iOS application...which would still force me to find a way to encrypt the bytes and then send encrypted bytes across. – Alexandru Jun 17 '14 at 17:32
  • Now that I think about it, I *could* have one byte before each integer represent the length of bytes that need to be compounded into a bytestream and then decrypted. I'll post an answer in a little bit after I iron it out. Not sure how to explain it...its hard to put it into words...hang on...when I get code posted, I'll show you what I mean... – Alexandru Jun 17 '14 at 18:58
  • @Alexandru: Yes, you could. This is not going to be very secure, however, as you're encrypting each byte separately. Anyone who can view the encrypted data will be able to spot repeated bytes. It's *still* not clear whether you're just doing this for the sake of interest or whether you're hoping to use this in the real world. (Just don't; *please* don't.) – Jon Skeet Jun 17 '14 at 18:59
  • If I say that it is for production, you'll stop helping me out on this most interesting topic. If I say its for my own curiosity, you'll reiterate that I should use an existing library. Either way, we would start a debate between the two of us. I don't mind debating, but it wouldn't lead to a completely low-level implementation of the problem at hand, would it? Regarding cryptography...I think that the best encryption algorithms and cyphers are those that nobody knows about. From first principles you can already tell that basing a solution on RSA would least introduce the problem of factoring. – Alexandru Jun 17 '14 at 19:10
  • @Alexandru: No, if it's for your own curiosity, it's fine. If it's for *production* then you should *absolutely* use an existing library. "I think that the best encryption algorithms and cyphers are those that nobody knows about" - not at all. If you decide to invent your own encryption algorithm, it's almost *certainly* not going to be secure if you don't have years of crypto experience (both in implementation and maths). But there's no way that in one Stack Overflow question we'll get from where you've started to a compliant RSA implementation. – Jon Skeet Jun 17 '14 at 19:13
  • @Alexandru: You might want to read http://www.dimgt.com.au/rsa_alg.html#pkcs1schemes for more details though. – Jon Skeet Jun 17 '14 at 19:14
  • Oh, one thing too regarding libraries...I'm trying to build a multiplayer online games that's cross-platform over TCP sockets. I'm not sure if there is any library for RSA encryption that works across all major devices: BlackBerry, Android, iOS, and Windows Phone. – Alexandru Jun 17 '14 at 19:41
  • 1
    @Alexandru: That's the point - if you're using a standard encryption algorithm, you don't *need* the same library on all platforms, you just need an implementation of the algorithm (with appropriate padding etc options) on all platforms. Given that it sounds like this *is* for something real, you would *definitely* be best off using a standard approach. Or, hey, just use TLS... – Jon Skeet Jun 17 '14 at 19:43
  • Ah, I never thought about using TLS. I'll try and use it if its supported across all platforms natively. – Alexandru Jun 17 '14 at 19:56
  • On that note Jon, there's no point in buying an SSL certificate for my needs, for TLS. Check this question out when you get the time, I posted it on the security exchange: http://security.stackexchange.com/questions/62433/isnt-this-the-same-as-using-a-certification-authority – Alexandru Jul 04 '14 at 01:22
1

Note: I updated this answer. Please scroll down to the update for how it should actually be implemented because this first way of doing it is not the correct way of doing RSA encryption.

One way I can think to do it is like this (but may not be compliant to standards), and also, note this does not pad:

public byte[] Encrypt(string m, Encoding encoding)
{
    byte[] bytes = encoding.GetBytes(m);
    byte[] returnBytes = new byte[0];
    for (int i = 0; i < bytes.Length; i++)
    {
        byte[] result = BigInteger.ModPow(bytes[i], (BigInteger)e, n).ToByteArray();
        int preSize = returnBytes.Length;
        Array.Resize(ref returnBytes, returnBytes.Length + result.Length + 1);
        (new byte[] { (byte)(result.Length) }).CopyTo(returnBytes, preSize);
        result.CopyTo(returnBytes, preSize + 1);
    }
    return returnBytes;
}

public string Decrypt(byte[] c, Encoding encoding)
{
    byte[] returnBytes = new byte[0];
    for (int i = 0; i < c.Length; i++)
    {
        int dataLength = (int)c[i];
        byte[] result = new byte[dataLength];
        for (int j = 0; j < dataLength; j++)
        {
            i++;
            result[j] = c[i];
        }
        BigInteger integer = new BigInteger(result);
        byte[] integerResult = BigInteger.ModPow(integer, d, n).ToByteArray();
        int preSize = returnBytes.Length;
        Array.Resize(ref returnBytes, returnBytes.Length + integerResult.Length);
        integerResult.CopyTo(returnBytes, preSize);
    }
    string decryptedString = encoding.GetString(returnBytes);
    return decryptedString;
}

This has the potential of being cross-platform because you have the option of using a different datatype to represent e or n and pass it to a C# back-end service like that. Here is a test:

string stringToEncrypt = "Mary had a little lamb.";
Console.WriteLine("Encrypting the string: {0}", stringToEncrypt);
byte[] encryptedBytes = engine.Encrypt(stringToEncrypt, Encoding.UTF8);
Console.WriteLine("Encrypted text: {0}", Encoding.UTF8.GetString(encryptedBytes));
Console.WriteLine("Decrypted text: {0}", engine.Decrypt(encryptedBytes, Encoding.UTF8));

Output:

Encrypting the string: Mary had a little lamb.
Encrypted text: ☻6☻1♦☻j☻☻&♀☻g♦☻t☻☻1♦☻?  ☻g♦☻1♦☻g♦☻?♥☻?☻☻7☺☻7☺☻?♥☻?♂☻g♦☻?♥☻1♦☻$☺☻
c       ☻?☻
Decrypted text: Mary had a little lamb.

Update: Everything I said earlier is completely wrong in the implementation of RSA. Wrong, wrong, wrong! This is the correct way to do RSA encryption:

  • Convert your string to a BigInteger datatype.
  • Make sure your integer is smaller than the value of n that you've calculated for your algorithm, otherwise you won't be able to decypher it.
  • Encrypt the integer. RSA works on integer encryption only. This is clear.
  • Decrypt it from the encrypted integer.
  • I can't help but wonder that the BigInteger class was mostly created for cryptography.

As an example:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace BytePadder
{
    class Program
    {
        const int p = 61;
        const int q = 53;
        const int n = 3233;
        const int totient = 3120;
        const int e = 991;
        const int d = 1231;

        static void Main(string[] args)
        {
            // ---------------------- RSA Example I ----------------------
            // Shows how an integer gets encrypted and decrypted.
            BigInteger integer = 1000;
            BigInteger encryptedInteger = Encrypt(integer);
            Console.WriteLine("Encrypted Integer: {0}", encryptedInteger);
            BigInteger decryptedInteger = Decrypt(encryptedInteger);
            Console.WriteLine("Decrypted Integer: {0}", decryptedInteger);
            // --------------------- RSA Example II ----------------------
            // Shows how a string gets encrypted and decrypted.
            string unencryptedString = "A";
            BigInteger integer2 = new BigInteger(Encoding.UTF8.GetBytes(unencryptedString));
            Console.WriteLine("String as Integer: {0}", integer2);
            BigInteger encryptedInteger2 = Encrypt(integer2);
            Console.WriteLine("String as Encrypted Integer: {0}", encryptedInteger2);
            BigInteger decryptedInteger2 = Decrypt(encryptedInteger2);
            Console.WriteLine("String as Decrypted Integer: {0}", decryptedInteger2);
            string decryptedIntegerAsString = Encoding.UTF8.GetString(decryptedInteger2.ToByteArray());
            Console.WriteLine("Decrypted Integer as String: {0}", decryptedIntegerAsString);
            Console.ReadLine();
        }

        static BigInteger Encrypt(BigInteger integer)
        {
            if (integer < n)
            {
                return BigInteger.ModPow(integer, e, n);
            }
            throw new Exception("The integer must be less than the value of n in order to be decypherable!");
        }

        static BigInteger Decrypt(BigInteger integer)
        {
            return BigInteger.ModPow(integer, d, n);
        }
    }
}

Example output:

Encrypted Integer: 1989
Decrypted Integer: 1000
String as Integer: 65
String as Encrypted Integer: 1834
String as Decrypted Integer: 65
Decrypted Integer as String: A
Alexandru
  • 12,264
  • 17
  • 113
  • 208
0

If you are looking to use RSA encryption in C# then you should not be attempting to build your own. For starters the prime numbers you have chosen are probably to small. P and Q are supposed to be large prime numbers.

You should check out some other question/answers:

how to use RSA to encrypt files (huge data) in C#

RSA Encryption of large data in C#

And other references: http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsacryptoserviceprovider.encrypt(v=vs.110).aspx

http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsacryptoserviceprovider.aspx

Community
  • 1
  • 1
Chad Dienhart
  • 5,024
  • 3
  • 23
  • 30
  • The size of p and q must be large only for security reasons. The smaller they are, the easier it is to brute-force attack your encryption. I was trying to use smaller values because its easier to verify primality and co-primality of smaller numbers than it is to verify it for larger ones, since its only a demonstration. – Alexandru Jun 17 '14 at 19:30
  • I'm so sorry, I was partly wrong on this. The only restriction is that your message (m) that you encrypt needs to be smaller than the value of n. Tested myself, and taken from: http://en.wikipedia.org/wiki/RSA_(cryptosystem) – Alexandru Jun 18 '14 at 12:49