Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Porting Java encryption routine to C#

I'm attempting with little success to port over Google's code to generate a secure token for their captcha (https://github.com/google/recaptcha-java/blob/master/appengine/src/main/java/com/google/recaptcha/STokenUtils.java):

The original utility has the following:

private static final String CIPHER_INSTANCE_NAME = "AES/ECB/PKCS5Padding";

private static String encryptAes(String input, String siteSecret) {
    try {
      SecretKeySpec secretKey = getKey(siteSecret);
      Cipher cipher = Cipher.getInstance(CIPHER_INSTANCE_NAME);
      cipher.init(Cipher.ENCRYPT_MODE, secretKey);
      return BaseEncoding.base64Url().omitPadding().encode(cipher.doFinal(input.getBytes("UTF-8")));
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

private static SecretKeySpec getKey(String siteSecret){
    try {
      byte[] key = siteSecret.getBytes("UTF-8");
      key = Arrays.copyOf(MessageDigest.getInstance("SHA").digest(key), 16);
      return new SecretKeySpec(key, "AES");
    } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
      e.printStackTrace();
    }
    return null;
  }

public static void main(String [] args) throws Exception {
    //Hard coded the following to get a repeatable result
    String siteSecret = "12345678";
    String jsonToken = "{'session_id':'abf52ca5-9d87-4061-b109-334abb7e637a','ts_ms':1445705791480}";
    System.out.println(" json token: " + jsonToken);
    System.out.println(" siteSecret: " + siteSecret);
    System.out.println(" Encrypted stoken: " + encryptAes(jsonToken, siteSecret));

Given the values I hardcoded, I get Irez-rWkCEqnsiRLWfol0IXQu1JPs3qL_G_9HfUViMG9u4XhffHqAyju6SRvMhFS86czHX9s1tbzd6B15r1vmY6s5S8odXT-ZE9A-y1lHns" back as my encrypted token.

My Java and crypto skills are more than a little rusty, and there aren't always direct analogs in C#. I attempted to merge encrypeAes() and getKey() with the following, which isn't correct:

public static string EncryptText(string PlainText, string siteSecret)
{
    using (RijndaelManaged aes = new RijndaelManaged())
    {
        aes.Mode = CipherMode.ECB;
        aes.Padding = PaddingMode.PKCS7;
        var bytes = Encoding.UTF8.GetBytes(siteSecret);
        SHA1 sha1 = SHA1.Create();
        var shaKey = sha1.ComputeHash(bytes);

        byte[] targetArray = new byte[16];
        Array.Copy(shaKey, targetArray, 16);

        aes.Key = targetArray;

        ICryptoTransform encrypto = aes.CreateEncryptor();

        byte[] plainTextByte = ASCIIEncoding.UTF8.GetBytes(PlainText);
        byte[] CipherText = encrypto.TransformFinalBlock(plainTextByte, 0, plainTextByte.Length);
        return HttpServerUtility.UrlTokenEncode(CipherText); //Equivalent to java's BaseEncoding.base64Url()?
    }
}

The C# version produces the incorrect value of: Ye+fySvneVUZJXth67+Si/e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ/430LgYcathLLd9U=

like image 801
Mister Epic Avatar asked Mar 15 '23 08:03

Mister Epic


1 Answers

Your code almost works as expected. It's just that you somehow mixed up the outputs of the Java version (and possibly the C# version).

If I execute your Java code (JDK 7 & 8 with Guava 18.0), I get

Ye-fySvneVUZJXth67-Si_e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ_430LgYcathLLd9U

and if I execute your C# code (DEMO), I get

Ye-fySvneVUZJXth67-Si_e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ_430LgYcathLLd9U1

So, the C# version has an additional "1" at the end. It should be a padding character, but isn't. This means that HttpServerUtility.UrlTokenEncode() doesn't provide a standards conform URL-safe Base64 encoding and you shouldn't use it. See also this Q&A.

The URL-safe Base64 encoding can be easily derived from the normal Base64 encoding (compare tables 1 and 2 in RFC4648) as seen in this answer by Marc Gravell:

string returnValue = System.Convert.ToBase64String(toEncodeAsBytes)
        .TrimEnd(padding).Replace('+', '-').Replace('/', '_');

with:

static readonly char[] padding = { '=' };

That's not all. If we take your Java output of

Ye+fySvneVUZJXth67+Si/e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ/430LgYcathLLd9U=

and decrypt it, then we get the following token:

{"session_id":"4182e173-3a24-4c10-b76c-b85a36be1173","ts_ms":1445786965574}

which is different from the token that you have in your code:

{'session_id':'abf52ca5-9d87-4061-b109-334abb7e637a','ts_ms':1445705791480}

The main remaining problem is that you're using invalid JSON. Strings and keys in JSON need to be wrapped in " and not '.

Which means that the encrypted token actually should have been (using a valid version of the token from your code):

D9rOP07fYgBfza5vbGsvdPe8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBsAWBDgtdSozv4jS_auBU-CgjlrJ_430LgYcathLLd9U
like image 55
Artjom B. Avatar answered Mar 19 '23 11:03

Artjom B.