Decrypting JSON Web Encrypted tokens in PCL

This week I worked on a feature for a Xamarin app I’m building that gave me quite some headaches because it involved a good amount of yak shaving to get it working correctly. My main reason for blogging this is for my own reference in the future, but maybe it can help someone else too 🙂

yak shaving
Yak shaving, gotta love it…

Our team is building a cross platform Xamarin app that has a good amount of shared code in an MVVM architecture. Most of the app is implemented in PCL libraries with a front end project per platform.

For communicating with a backend API, the app requires a token obtained from an Identity Provider. In addition to the API access token, we’re also getting an OpenId token that is crucial for the app. Using Xamarin.Auth for example, it’s pretty easy to setup an OAuth flow that lets the user login and the app obtain that token.

So far so good.

There’s a lot to be said about which OAuth flow is suitable in which scenario. For us, embedding a client secret in the app was a no go, since this can’t be kept secret if it’s embedded in the source code of the app (reverse engineering!). We chose a PCKE approach. Without going into too much detail around the flow, I want to focus on one aspect: securing the OpenID token. This blogpost is mostly about what I learned about token encryption and decryption.

207h

Our requirement was to have asymmetric encryption in place, where the app provides a public key to the server, which it uses to encrypt the JWT. The app can then decrypt the data using its private key, so we could mitigate a token interception attack. The public key was to be transferred in an X509 certificate (PEM encoded). The idea was to have the app generate a private/public keypair on-the-fly as we don’t want to hard code a private key into the app. That would lead us to the same situation as the hard coded Client Secret. All we want is to make sure that the requesting app is the only one able to decrypt the token it requested.

There’s a nice standard for encrypting tokens using a variety of algorithms: JWE – JSON Web Encryption. The original token is in a JSON structure, encrypted and packaged into a standardized format. In our case, these were the specs:

Algorithm: RSA_OAEP – RSAES using Optimal Asymmetric Encryption Padding (OAEP) (RFC 3447), with the default parameters specified by RFC 3447 in section A.2.1

Encryption method:  A256GCM – AES in Galois/Counter Mode (GCM) (NIST.800-38D) using a 256-bit key

Basically what happens is: the token payload is encrypted using a random Content Encryption Key (CEK) provided by the server. This is an authenticated encryption mode, where data is added to prove authenticity. In addition to the key, an Initialization Vector (IV) is added, and also an Authentication Tag (authTag) and Additional Authenticated Data, basically consisting of a Base64 representation of the JWE header. All this together provides a symmetric encryption through which we can decrypt the token. This is the A256CGM – AES step.

All the information I mentioned – the CEK, IV, cipherText (the encrypted token), AAD and authTag – are present in the JWE package. To add the final layer of security, the CEK has to be encrypted, otherwise anyone would be able to read the token. This is the RSA_OAEP – RSAES step mentioned earlier. In plain English, the CEK is encrypted using asymmetric encryption. The server uses the app’s public key for this, obtained from the X509 certificate in the request.

So the way back to obtain the token plain text is to decrypt the CEK using the private key, and then decrypt the token payload using the CEK together with the IV, AAD and authTag, as illustrated in the diagram below:

jwe-decryption

That looks pretty daunting, but luckily there are libraries that help us handle this scenario, or the many other combinations of algorithms and encodings. One popular example is the JOSE-JWT library for .NET. You can feed it the complete JWE package and the CEK decryption key, and it does all the heavy lifting. JOSE-JWT can also handle our scenario, with our combination of algorithms and encodings.

Here’s the catch…

This won’t fly in a PCL. JOSE-JWT is built on System.Security.Cryptography, which isn’t available in PCL code. There have been some attempts to make JOSE-JWT available for Xamarin, but this won’t work fully cross platform. So ideally, we’d want a library that solves this using PCL compatible API’s. And sadly, there is no JOSE-JWT implementation available for PCL yet.

281H_banner.png
Now what?!

JWE just uses standard encryption algorithms, so it should be possible to implement these on top of a crypto API that is available cross platform (in PCL). I have tried two options:

  • PCLCrypto: this library mimics the WinRTCrypto API’s but relies on native, platform specific crypto-engines to perform the encryption. This results in the fastest encryption engine but is limited in terms of supported scenario’s or algorithms.
  • BouncyCastle-PCL: a cross platform crypto library that supports all sorts of cryptographic algorithms.

Since PCLCrypto mimics the WinRTCrypto API, it seemed pretty easy to implement our JOSE-JWT port on top of PCLCrypto, by stealing borrowing from the experimental JOSE-RT port. (Thanks Dmitriy Vsekhvalnov for the tip!) And indeed, if you look at the AesGcmEncryptor class, it looks rather straight forward. Alas, it turns out our GCM authenticated mode isn’t supported by PCLCrypto. It threw a NotSupportedException at me.

So the other option was to use BouncyCastle-PCL. We ended up with a solution that does the following:

  • Generation of the public/private keypair
  • Generation of the X509 certificate containing the public key
  • Unpacking the JWE package
  • Decrypting (or unwrapping) the CEK using the private key
  • Decrypting the token payload

Mind you, I only had to support our specific scenario, but it’s a small step towards a full JOSE-PCL implementation. Maybe someday I’ll make an attempt 🙂

Without further ado, here is the code I ended up with:

using System;
using System.IO;
using System.Text;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Encodings;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.Utilities.IO.Pem;
using Org.BouncyCastle.X509;
using System.Linq;

namespace JoseJWE
{
	public class CryptoService
	{
		public AsymmetricCipherKeyPair GenerateKeyPair()
		{
			var random = new SecureRandom();
			var keyGenerationParameters = new KeyGenerationParameters(random, 1024);
			var keyPairGenerator = new RsaKeyPairGenerator();
			keyPairGenerator.Init(keyGenerationParameters);
			var keyPair = keyPairGenerator.GenerateKeyPair();
			return keyPair;
		}

		public string GeneratePemEncodedCertificate(AsymmetricCipherKeyPair keyPair)
		{
				var random = new SecureRandom();
				var signatureFactory = new Asn1SignatureFactory("SHA256WithRSA", keyPair.Private, random);

				var gen = new X509V3CertificateGenerator();
				gen.SetPublicKey(keyPair.Public);

				BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
				gen.SetSerialNumber(serialNumber);

				var x509Name = new X509Name("CN=MyCertificate,O=RoyCornelissen,OU=CryptoService");
				gen.SetIssuerDN(x509Name);
				gen.SetSubjectDN(x509Name);

				gen.SetNotBefore(DateTime.UtcNow.AddHours(-1));
				gen.SetNotAfter(DateTime.UtcNow.AddMonths(1));

				var x509 = gen.Generate(signatureFactory);
				x509.CheckValidity(DateTime.UtcNow);
				x509.Verify(keyPair.Public);

				using (var stringWriter = new StringWriter())
				{
					var writer = new PemWriter(stringWriter);
					var pog = new PemObject("CERTIFICATE", x509.GetEncoded());
					writer.WriteObject(pog);
					return stringWriter.ToString();
				}
		}

		public string DecodeJwt(string tokenData, AsymmetricKeyParameter privateKey)
		{
			var token = Parse(tokenData);
			return DecodeAndDecrypt(token, privateKey);
		}

		private string DecodeAndDecrypt(byte[][] parts, AsymmetricKeyParameter key)
		{
			byte[] header = parts[0];
			byte[] encryptedCek = parts[1];
			byte[] iv = parts[2];
			byte[] cipherText = parts[3];
			byte[] authTag = parts[4];

			var cek = Unwrap(encryptedCek, key);
			var aad = Encoding.UTF8.GetBytes(Serialize(header));

			return Decrypt(cek, iv, aad, cipherText, authTag);
		}

		private string Serialize(params byte[][] parts)
		{
			var builder = new StringBuilder();

			foreach (var part in parts)
			{
				builder.Append(Base64UrlEncode(part)).Append(".");
			}

			builder.Remove(builder.Length - 1, 1);

			return builder.ToString();
		}

		private byte[][] Parse(string token)
		{
			string[] parts = token.Split('.');

			var result = new byte[parts.Length][];

			for (int i = 0; i < parts.Length; i++)
			{
				result[i] = Base64UrlDecode(parts[i]);
			}

			return result;
		}

		private byte[] Unwrap(byte[] encryptedCek, AsymmetricKeyParameter key)
		{
			var decryptEngine = new OaepEncoding(new RsaEngine());
			decryptEngine.Init(false, key);
			var deciphered = decryptEngine.ProcessBlock(encryptedCek, 0, encryptedCek.Length);
			return deciphered;
		}

		//Preconfigured Encryption Parameters
		private static readonly int MacBitSize = 128;

		/// <summary>
		/// Performs AES decryption in GCM chaining mode over cipher text
		/// </summary>
		/// <param name="cek">aes key</param>
		/// <param name="iv">initialization vector</param>
		/// <param name="aad">additional authn data</param>
		/// <param name="cipherText">cipher text message to be decrypted</param>
		/// <param name="authTag">authentication tag</param>
		/// <returns>decrypted plain text messages</returns>
		private string Decrypt(byte[] cek, byte[] iv, byte[] aad, byte[] cipherText, byte[] authTag)
		{
				var keyParameter = new KeyParameter(cek);
				var gcmParameters = new AeadParameters(
					keyParameter,
					MacBitSize,
					iv);

				var gcmMode = new GcmBlockCipher(new AesFastEngine());
				gcmMode.Init(false, gcmParameters);
				gcmMode.ProcessAadBytes(aad, 0, aad.Length);

				var cipherBuffer = cipherText.Concat(authTag).ToArray();
				var plainBytes = new byte[gcmMode.GetOutputSize(cipherBuffer.Length)];
				var res = gcmMode.ProcessBytes(cipherBuffer, 0, cipherBuffer.Length, plainBytes, 0);
				gcmMode.DoFinal(plainBytes, res);

				var plain = Encoding.UTF8.GetString(plainBytes, 0, plainBytes.Length);
				return plain;
		}

		// from JWT spec
		public byte[] FromBase64Url(string base64Url)
		{
			string padded = base64Url.Length % 4 == 0
				? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
			string base64 = padded.Replace("_", "/")
									.Replace("-", "+");
			return Convert.FromBase64String(base64);
		}

		// from JWT spec
		public string Base64UrlEncode(byte[] input)
		{
			var output = Convert.ToBase64String(input);
			output = output.Split('=')[0]; // Remove any trailing '='s
			output = output.Replace('+', '-'); // 62nd char of encoding
			output = output.Replace('/', '_'); // 63rd char of encoding
			return output;
		}

		// from JWT spec
		private byte[] Base64UrlDecode(string input)
		{
			var output = input;
			output = output.Replace('-', '+'); // 62nd char of encoding
			output = output.Replace('_', '/'); // 63rd char of encoding
			switch (output.Length % 4) // Pad with trailing '='s
			{
				case 0: break; // No pad chars in this case
				case 1: output += "==="; break; // Three pad chars
				case 2: output += "=="; break; // Two pad chars
				case 3: output += "="; break; // One pad char
				default: throw new Exception("Illegal base64url string!");
			}
			var converted = Convert.FromBase64String(output); // Standard base64 decoder
			return converted;
		}
	}
}

Its usage is demonstrated by the following unit tests:


<pre>using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using Jose;
using NUnit.Framework;
using Org.BouncyCastle.Crypto.Parameters;

namespace JoseJWE.Tests
{
	[TestFixture]
	public class CryptoServiceTests
	{
		private const string TokenPlainText = "{\"sub\":\"roycornelissen\",\"aud\":\"sample app\",\"nbf\":136424444,\"iss\":\"https://api.someorganization.com\",\"preferred_username\":\"Roy Cornelissen\",\"exp\":1364293137,\"given_name\":\"Roy\",\"iat\":13642555,\"family_name\":\"Cornelissen\",\"preferred_language\":\"nl-NL\"}";

		[Test]
		public void GenerateCertificate_Generates_Valid_X509Certificate()
		{
			var g = new CryptoService();
			var keyPair = g.GenerateKeyPair();

			var pemEncodedCertificate = g.GeneratePemEncodedCertificate(keyPair).ToString();

			var p = new Org.BouncyCastle.X509.X509CertificateParser();
			var certDecoded = p.ReadCertificate(Encoding.UTF8.GetBytes(pemEncodedCertificate));

			certDecoded.Should().NotBeNull();
			certDecoded.NotBefore.Should().BeBefore(DateTime.UtcNow);
			certDecoded.NotAfter.Should().BeAfter(DateTime.UtcNow);
		}

		[Test]
		public void Certificate_Used_For_JWT_Encryption_JWE_Can_Be_Decrypted()
		{
			var g = new CryptoService();
			var keypair = g.GenerateKeyPair();

			var cert = g.GeneratePemEncodedCertificate(keypair);
			var base64Certificate = g.Base64UrlEncode(Encoding.UTF8.GetBytes(cert));

			// try to perform local encryption and decryption for reference
			var p = new Org.BouncyCastle.X509.X509CertificateParser();
			var certDecoded = p.ReadCertificate(g.FromBase64Url(base64Certificate));

      // use 3rd party library JOSE-JWT to encode the JWT (only works in .NET, not from PCL!)
			var publicRsaKey = ToRSA((RsaKeyParameters)certDecoded.GetPublicKey());
			var encrypted = JWT.Encode(TokenPlainText, publicRsaKey, JweAlgorithm.RSA_OAEP, JweEncryption.A256GCM);

			// now attempt to decode it using our own cryptoService
			var plainText = g.DecodeJwt(encrypted, keypair.Private);
			plainText.Should().Be(TokenPlainText);
		}
    
    public static RSA ToRSA(RsaKeyParameters rsaKey)
    {
        RSAParameters rp = ToRSAParameters(rsaKey);
        RSACryptoServiceProvider rsaCsp = new RSACryptoServiceProvider();
        rsaCsp.ImportParameters(rp);
        return rsaCsp;
    }

    private static RSAParameters ToRSAParameters(RsaKeyParameters rsaKey)
    {
        RSAParameters rp = new RSAParameters();
        rp.Modulus = rsaKey.Modulus.ToByteArrayUnsigned();
        if (rsaKey.IsPrivate)
            rp.D = rsaKey.Exponent.ToByteArrayUnsigned();
        else
            rp.Exponent = rsaKey.Exponent.ToByteArrayUnsigned();
        return rp;
    }
	}
}</pre>

The entire Gist is here on GitHub.

Advertisements