Skip to content

Instantly share code, notes, and snippets.

@roycornelissen
Last active October 12, 2017 11:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save roycornelissen/c59dfca22d813e96ded28ace93e02fcb to your computer and use it in GitHub Desktop.
Save roycornelissen/c59dfca22d813e96ded28ace93e02fcb to your computer and use it in GitHub Desktop.
JOSE-JWE implementation for PCL on top of BouncyCastle-PCL
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;
}
}
}
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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment