Last active
October 12, 2017 11:48
-
-
Save roycornelissen/c59dfca22d813e96ded28ace93e02fcb to your computer and use it in GitHub Desktop.
JOSE-JWE implementation for PCL on top of BouncyCastle-PCL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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