4

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=

Mister Epic
  • 16,295
  • 13
  • 76
  • 147
  • Implemented SHA1 hashing, still no joy. – Mister Epic Oct 25 '15 at 17:13
  • You could try using [`AesManaged`](https://msdn.microsoft.com/en-us/library/system.security.cryptography.aesmanaged%28v=vs.100%29.aspx) instead of `RijndaelManaged` (which has different key size by default and thus is not the *standard* AES) in order to match the Java part `CIPHER_INSTANCE_NAME = "AES/ECB/PKCS5Padding"` and use [`Convert.ToBase64String`](https://msdn.microsoft.com/en-us/library/dhx0d524%28v=vs.110%29.aspx) instead of `HttpServerUtility.UrlTokenEncode...`. – keenthinker Oct 25 '15 at 17:20
  • Updated to include c# output. @pasty I updated to `AESManaged`, but am unclear about how I would align with the cipher name. I opted for `PaddingMode.PKCS7` per http://blog.zebsadiq.com/post/AESCBCPKCS5Padding-EncryptionDecryption-in-C.aspx, but I admit I am fumbling along. Changing to `Convert.ToBase64String` had no effect on the result. I opted for `HttpServerUtility.UrlTokenEncode` as it seemed closest to Java's BaseEncoding.base64Url(). – Mister Epic Oct 25 '15 at 17:24
  • @pasty The block size of RijndaelManaged is [128 bit by default](http://ideone.com/7Irrxz), so it is functionally equivalent to AesManaged. – Artjom B. Oct 25 '15 at 17:48
  • @ArtjomB. thanks for the tip, I thought it is 256. – keenthinker Oct 25 '15 at 17:51
  • I'm an encryption noob and a while ago I had to port Java encryption to C# so I was kind of lost. Then I used a library called BouncyCastle (Originally Java library but has also C# version). I think it has your algorithm as CipherUtilities.GetCipher("AES/ECB/PKCS5PADDING"); is not crashing and returns a cipher, so it might be worth to take a look at it http://www.bouncycastle.org/csharp/ ,however I might be terribly wrong :) – kirotab Oct 27 '15 at 19:50
  • This Java code produces `Ye-fySvneVUZJXth67-Si_e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ_430LgYcathLLd9U` which is the same as the C# version with the difference that a URL-safe Base64 table was used. So there is something wrong with your Java output. Are you really sure that you get that output? If so, what versions of JDK and Guava do you have? – Artjom B. Oct 27 '15 at 20:27
  • @ArtjomB. going to validate when I get home. – Mister Epic Oct 27 '15 at 20:29
  • Also `HttpServerUtility.UrlTokenEncode is Base64`... Any thought how I would make that "URL-safe" ? – Mister Epic Oct 27 '15 at 20:39
  • Yes, [this answer](http://stackoverflow.com/a/26354677). Also, I decrypted your Java example and the token is `{"session_id":"4182e173-3a24-4c10-b76c-b85a36be1173","ts_ms":1445786965574}` which is different from the one in your code and also valid JSON. The JSON token that is in your code is not valid JSON, because strings need to be wrapped in `"` and not `'`. – Artjom B. Oct 27 '15 at 20:44
  • @ArtjomB. Must be the long hours as a new dad - I must have mucked things up moving code around trying to get this working. Don't know how I stumbled into generating the right token, but please submit an answer and I'll award the bounty, if only for your patience in walking through this. – Mister Epic Oct 27 '15 at 22:45

2 Answers2

4

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
Community
  • 1
  • 1
Artjom B.
  • 61,146
  • 24
  • 125
  • 222
3

Here's a C# implementation that reproduces the same result as your Java code:

class Program
{
    public static byte[] GetKey(string siteSecret)
    {
        byte[] key = Encoding.UTF8.GetBytes(siteSecret);
        return SHA1.Create().ComputeHash(key).Take(16).ToArray();
    }

    public static string EncryptAes(string input, string siteSecret)
    {
        var key = GetKey(siteSecret);
        using (var aes = AesManaged.Create())
        {
            if (aes == null) return null;

            aes.Mode = CipherMode.ECB;
            aes.Padding = PaddingMode.PKCS7;
            aes.Key = key;
            byte[] inputBytes = Encoding.UTF8.GetBytes(input);

            var enc = aes.CreateEncryptor(key, new byte[16]);
            return UrlSafeBase64(enc.TransformFinalBlock(inputBytes,0,input.Length));
        }
    }

    // http://stackoverflow.com/a/26354677/162671
    public static string UrlSafeBase64(byte[] bytes)
    {
        return Convert.ToBase64String(bytes).TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }
    static void Main(string[] args)
    {
        string siteSecret = "12345678";
        string jsonToken = "{'session_id':'abf52ca5-9d87-4061-b109-334abb7e637a','ts_ms':1445705791480}";

        Console.WriteLine(" json token: " + jsonToken);
        Console.WriteLine(" siteSecret: " + siteSecret);
        Console.WriteLine(EncryptAes(jsonToken, siteSecret));
        Console.ReadLine();
    }
}

I don't know why you said you're getting Irez-rWkCEqnsiRLWfol0IXQu1JPs3qL_G_9HfUViMG9u4XhffHqAyju6SRvMhFS86czHX9s1tbzd6B15r1vmY6s5S8odXT-ZE9A-y1lHns from the Java program because I'm not getting that output. The output I'm getting from both the C# version and the Java version is this:

Ye-fySvneVUZJXth67-Si_e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ_430LgYcathLLd9U

As you can see here:

Screenshot of the programs output. Top is the C# version and bottom is the Java version (IntelliJ Idea 14.1.5)

The Java version was copy/pasted from your code and is using guava-18.0 and compiled with JDK8 x64 (I'm not a java expert so I'm just adding these in case it makes any difference).

Nasreddine
  • 36,610
  • 17
  • 75
  • 94