From 17310b0b78b031c3de256c1c240710b640cc8fc3 Mon Sep 17 00:00:00 2001 From: panico Date: Fri, 18 Dec 2020 13:46:12 +0100 Subject: [PATCH 1/7] Remove bouncycastle dependency --- WebPush.Test/ECKeyHelperTest.cs | 61 ++-- WebPush.Test/JWSSignerTest.cs | 4 +- WebPush.Test/WebPush.Test.csproj | 8 +- WebPush/Model/AsymmetricKeyPair.cs | 14 + WebPush/Model/WebPushException.cs | 13 +- .../PublishProfiles/FolderProfile.pubxml | 8 +- WebPush/Util/ECKeyHelper.cs | 150 ++++++--- WebPush/Util/Encryptor.cs | 298 +++++++++--------- WebPush/Util/JwsSigner.cs | 59 ++-- WebPush/VapidHelper.cs | 32 +- WebPush/WebPush.csproj | 8 +- WebPush/WebPushClient.cs | 2 +- 12 files changed, 368 insertions(+), 289 deletions(-) create mode 100644 WebPush/Model/AsymmetricKeyPair.cs diff --git a/WebPush.Test/ECKeyHelperTest.cs b/WebPush.Test/ECKeyHelperTest.cs index ae2ef6d..21bb27f 100644 --- a/WebPush.Test/ECKeyHelperTest.cs +++ b/WebPush.Test/ECKeyHelperTest.cs @@ -1,6 +1,6 @@ using System.Linq; +using System.Security.Cryptography; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Org.BouncyCastle.Crypto.Parameters; using WebPush.Util; namespace WebPush.Test @@ -16,12 +16,11 @@ public class ECKeyHelperTest [TestMethod] public void TestGenerateKeys() { - var keys = ECKeyHelper.GenerateKeys(); + var keys = ECKeyHelper.GenerateKeys(); + var publicKey = keys.PublicKey; + var privateKey = keys.PrivateKey; - var publicKey = ((ECPublicKeyParameters) keys.Public).Q.GetEncoded(false); - var privateKey = ((ECPrivateKeyParameters) keys.Private).D.ToByteArrayUnsigned(); - - var publicKeyLength = publicKey.Length; + var publicKeyLength = publicKey.Length; var privateKeyLength = privateKey.Length; Assert.AreEqual(65, publicKeyLength); @@ -31,39 +30,41 @@ public void TestGenerateKeys() [TestMethod] public void TestGenerateKeysNoCache() { - var keys1 = ECKeyHelper.GenerateKeys(); - var keys2 = ECKeyHelper.GenerateKeys(); + var keys = ECKeyHelper.GenerateKeys(); + var publicKey1 = keys.PublicKey; + var privateKey1 = keys.PrivateKey; - var publicKey1 = ((ECPublicKeyParameters) keys1.Public).Q.GetEncoded(false); - var privateKey1 = ((ECPrivateKeyParameters) keys1.Private).D.ToByteArrayUnsigned(); + var keys2 = ECKeyHelper.GenerateKeys(); + var publicKey2 = keys2.PublicKey; + var privateKey2 = keys2.PrivateKey; - var publicKey2 = ((ECPublicKeyParameters) keys2.Public).Q.GetEncoded(false); - var privateKey2 = ((ECPrivateKeyParameters) keys2.Private).D.ToByteArrayUnsigned(); - Assert.IsFalse(publicKey1.SequenceEqual(publicKey2)); + Assert.IsFalse(publicKey1.SequenceEqual(publicKey2)); Assert.IsFalse(privateKey1.SequenceEqual(privateKey2)); } - [TestMethod] - public void TestGetPrivateKey() - { - var privateKey = UrlBase64.Decode(TestPrivateKey); - var privateKeyParams = ECKeyHelper.GetPrivateKey(privateKey); + [TestMethod] + public void TestGetPrivateKey() + { + #if NET48 + var privateKey = UrlBase64.Decode(TestPrivateKey); + var privateKeyParams = ECKeyHelper.GetPrivateKey(privateKey); - var importedPrivateKey = UrlBase64.Encode(privateKeyParams.D.ToByteArrayUnsigned()); + var importedPrivateKey = UrlBase64.Encode((privateKeyParams as ECDsaCng).ExportParameters(true).D); - Assert.AreEqual(TestPrivateKey, importedPrivateKey); - } + Assert.AreEqual(TestPrivateKey, importedPrivateKey); + #endif + } - [TestMethod] - public void TestGetPublicKey() - { - var publicKey = UrlBase64.Decode(TestPublicKey); - var publicKeyParams = ECKeyHelper.GetPublicKey(publicKey); + [TestMethod] + public void TestGetPublicKey() + { + var publicKey = UrlBase64.Decode(TestPublicKey); + var publicKeyParams = ECKeyHelper.GetPublicKey(publicKey); - var importedPublicKey = UrlBase64.Encode(publicKeyParams.Q.GetEncoded(false)); + var importedPublicKey = UrlBase64.Encode(publicKeyParams.GetECPublicKey()); - Assert.AreEqual(TestPublicKey, importedPublicKey); - } - } + Assert.AreEqual(TestPublicKey, importedPublicKey); + } + } } \ No newline at end of file diff --git a/WebPush.Test/JWSSignerTest.cs b/WebPush.Test/JWSSignerTest.cs index 184d755..f489280 100644 --- a/WebPush.Test/JWSSignerTest.cs +++ b/WebPush.Test/JWSSignerTest.cs @@ -14,9 +14,9 @@ public class JWSSignerTest public void TestGenerateSignature() { var decodedPrivateKey = UrlBase64.Decode(TestPrivateKey); - var privateKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); + var privateKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); - var header = new Dictionary(); + var header = new Dictionary(); header.Add("typ", "JWT"); header.Add("alg", "ES256"); diff --git a/WebPush.Test/WebPush.Test.csproj b/WebPush.Test/WebPush.Test.csproj index e49096a..0b7328c 100755 --- a/WebPush.Test/WebPush.Test.csproj +++ b/WebPush.Test/WebPush.Test.csproj @@ -1,15 +1,15 @@  - netcoreapp1.0;netcoreapp1.1;netcoreapp2.0 + net45;net46;net48; false - + - - + + diff --git a/WebPush/Model/AsymmetricKeyPair.cs b/WebPush/Model/AsymmetricKeyPair.cs new file mode 100644 index 0000000..47bfcd9 --- /dev/null +++ b/WebPush/Model/AsymmetricKeyPair.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WebPush +{ + public class AsymmetricKeyPair + { + public byte[] PublicKey; + public byte[] PrivateKey; + } +} diff --git a/WebPush/Model/WebPushException.cs b/WebPush/Model/WebPushException.cs index 80a893f..ddfa795 100644 --- a/WebPush/Model/WebPushException.cs +++ b/WebPush/Model/WebPushException.cs @@ -14,8 +14,17 @@ public WebPushException(string message, HttpStatusCode statusCode, HttpResponseH PushSubscription = pushSubscription; } - public HttpStatusCode StatusCode { get; set; } + public WebPushException(string message, HttpStatusCode statusCode, HttpResponseHeaders headers, + PushSubscription pushSubscription,string reasonPhrase) : this( + message,statusCode,headers,pushSubscription) + { + ReasonPhrase = reasonPhrase; + } + + public HttpStatusCode StatusCode { get; set; } public HttpResponseHeaders Headers { get; set; } public PushSubscription PushSubscription { get; set; } - } + public string ReasonPhrase { get; set; } + + } } \ No newline at end of file diff --git a/WebPush/Properties/PublishProfiles/FolderProfile.pubxml b/WebPush/Properties/PublishProfiles/FolderProfile.pubxml index 8af1ca3..04b211c 100644 --- a/WebPush/Properties/PublishProfiles/FolderProfile.pubxml +++ b/WebPush/Properties/PublishProfiles/FolderProfile.pubxml @@ -1,13 +1,13 @@  FileSystem Release - netstandard1.1 - bin\Release\PublishOutput + net45 + C:\net\NugetPackage + Any CPU \ No newline at end of file diff --git a/WebPush/Util/ECKeyHelper.cs b/WebPush/Util/ECKeyHelper.cs index 5655908..e3d2fa6 100644 --- a/WebPush/Util/ECKeyHelper.cs +++ b/WebPush/Util/ECKeyHelper.cs @@ -1,64 +1,112 @@ -using System; -using System.IO; -using Org.BouncyCastle.Asn1; -using Org.BouncyCastle.Asn1.Nist; -using Org.BouncyCastle.Crypto; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.OpenSsl; -using Org.BouncyCastle.Security; +using System.Security.Cryptography; +using System.Linq; namespace WebPush.Util { - internal static class ECKeyHelper - { - public static ECPrivateKeyParameters GetPrivateKey(byte[] privateKey) - { - Asn1Object version = new DerInteger(1); - Asn1Object derEncodedKey = new DerOctetString(privateKey); - Asn1Object keyTypeParameters = new DerTaggedObject(0, new DerObjectIdentifier(@"1.2.840.10045.3.1.7")); + internal static class ECKeyHelper + { + public static byte[] GetECPublicKey(this CngKey key){ + var cngKey = key.Export(CngKeyBlobFormat.EccPublicBlob); + return new byte[] { 0x04 }.Concat(cngKey.Skip(8)).ToArray(); + } - Asn1Object derSequence = new DerSequence(version, derEncodedKey, keyTypeParameters); + public static byte[] GetECPrivateKey(this CngKey key) + { + var cngKey = key.Export(CngKeyBlobFormat.EccPrivateBlob); + return cngKey.Skip(8 + 32 + 32).Take(32).ToArray(); + } - var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); - var pemKey = "-----BEGIN EC PRIVATE KEY-----\n"; - pemKey += base64EncodedDerSequence; - pemKey += "\n-----END EC PRIVATE KEY----"; + public static CngKey GetPublicKey(byte[] key) + { + var keyType = new byte[] { 0x45, 0x43, 0x4B, 0x31 }; + var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; - var reader = new StringReader(pemKey); - var pemReader = new PemReader(reader); - var keyPair = (AsymmetricCipherKeyPair) pemReader.ReadObject(); + var keyImport = keyType.Concat(keyLength).Concat(key.Skip(1)).ToArray(); - return (ECPrivateKeyParameters) keyPair.Private; - } + return CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob); + } +#if NET48 + public static AsymmetricAlgorithm GetPrivateKey(byte[] privateKey) + { + return ECDsaCng.Create(new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + D = privateKey, + Q = new ECPoint(){ X = new byte[32],Y = new byte[32]} + }); + } + public static AsymmetricKeyPair GenerateKeys() + { - public static ECPublicKeyParameters GetPublicKey(byte[] publicKey) - { - Asn1Object keyTypeParameters = new DerSequence(new DerObjectIdentifier(@"1.2.840.10045.2.1"), - new DerObjectIdentifier(@"1.2.840.10045.3.1.7")); - Asn1Object derEncodedKey = new DerBitString(publicKey); + using (var cng = new ECDiffieHellmanCng(ECCurve.NamedCurves.nistP256)) + { + cng.GenerateKey(ECCurve.NamedCurves.nistP256); + var parameters = cng.ExportParameters(true); + var pr = parameters.D.ToArray(); + var pub = new byte[] { 0x04 }.Concat(parameters.Q.X).Concat(parameters.Q.Y).ToArray(); + return new AsymmetricKeyPair() { PublicKey = pub,PrivateKey = pr }; + } + } +#else + private static CngKey ImportPrivCngKey(byte[] pubKey, byte[] privKey) + { + // to import keys to CngKey in ECCPublicKeyBlob and ECCPrivateKeyBlob format, keys should be form in specific formats as noted here : + // https://stackoverflow.com/a/24255090 + // magic prefixes : https://referencesource.microsoft.com/#system.core/System/Security/Cryptography/BCryptNative.cs,fde0749a0a5f70d8,references + var keyType = new byte[] { 0x45, 0x43, 0x53, 0x32 }; + var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; - Asn1Object derSequence = new DerSequence(keyTypeParameters, derEncodedKey); + var key = pubKey.Skip(1); - var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); - var pemKey = "-----BEGIN PUBLIC KEY-----\n"; - pemKey += base64EncodedDerSequence; - pemKey += "\n-----END PUBLIC KEY-----"; + var keyImport = keyType.Concat(keyLength).Concat(key).Concat(privKey).ToArray(); - var reader = new StringReader(pemKey); - var pemReader = new PemReader(reader); - var keyPair = pemReader.ReadObject(); - return (ECPublicKeyParameters) keyPair; - } + var cngKey = CngKey.Import(keyImport, CngKeyBlobFormat.EccPrivateBlob); + return cngKey; + } + public static ECDsaCng GetPrivateKey(byte[] privateKey) + { + var fakePubKey = new byte[64]; + var publicKey = (new byte[] { 0x04 }).Concat(fakePubKey).ToArray(); - public static AsymmetricCipherKeyPair GenerateKeys() - { - var ecParameters = NistNamedCurves.GetByName("P-256"); - var ecSpec = new ECDomainParameters(ecParameters.Curve, ecParameters.G, ecParameters.N, ecParameters.H, - ecParameters.GetSeed()); - var keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator("ECDH"); - keyPairGenerator.Init(new ECKeyGenerationParameters(ecSpec, new SecureRandom())); - - return keyPairGenerator.GenerateKeyPair(); - } - } + var cngKey = ImportPrivCngKey(publicKey, privateKey); + var ecDsaCng = new ECDsaCng(cngKey); + ecDsaCng.HashAlgorithm = CngAlgorithm.ECDsaP256; + return ecDsaCng; + } + + public static AsymmetricKeyPair GenerateKeys() + { + CngProvider cp = CngProvider.MicrosoftSoftwareKeyStorageProvider; + string keyName = "tempvapidkey"; + if (CngKey.Exists(keyName, cp)) + { + using (CngKey cngKey = CngKey.Open(keyName, cp)) + cngKey.Delete(); + } + CngKeyCreationParameters kcp = new CngKeyCreationParameters + { + Provider = cp, + ExportPolicy = CngExportPolicies.AllowPlaintextExport + }; + try + { + using (CngKey myKey = CngKey.Create(CngAlgorithm.ECDiffieHellmanP256, keyName, kcp)) + { + return new AsymmetricKeyPair() + { + PublicKey = myKey.GetECPublicKey(), + PrivateKey = myKey.GetECPrivateKey() + }; + } + } + finally{ + if (CngKey.Exists(keyName, cp)) + { + using (CngKey cngKey = CngKey.Open(keyName, cp)) + cngKey.Delete(); + } + } + } +#endif + } } \ No newline at end of file diff --git a/WebPush/Util/Encryptor.cs b/WebPush/Util/Encryptor.cs index d8b294f..ffce696 100644 --- a/WebPush/Util/Encryptor.cs +++ b/WebPush/Util/Encryptor.cs @@ -1,150 +1,164 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Text; -using Org.BouncyCastle.Crypto.Digests; -using Org.BouncyCastle.Crypto.Engines; -using Org.BouncyCastle.Crypto.Macs; -using Org.BouncyCastle.Crypto.Modes; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Security; +using Security.Cryptography; namespace WebPush.Util { - // @LogicSoftware - // Originally from https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/Encryptor.cs - internal static class Encryptor - { - public static EncryptionResult Encrypt(string userKey, string userSecret, string payload) - { - var userKeyBytes = UrlBase64.Decode(userKey); - var userSecretBytes = UrlBase64.Decode(userSecret); - var payloadBytes = Encoding.UTF8.GetBytes(payload); - - return Encrypt(userKeyBytes, userSecretBytes, payloadBytes); - } - - public static EncryptionResult Encrypt(byte[] userKey, byte[] userSecret, byte[] payload) - { - var salt = GenerateSalt(16); - var serverKeyPair = ECKeyHelper.GenerateKeys(); - - var ecdhAgreement = AgreementUtilities.GetBasicAgreement("ECDH"); - ecdhAgreement.Init(serverKeyPair.Private); - - var userPublicKey = ECKeyHelper.GetPublicKey(userKey); - - var key = ecdhAgreement.CalculateAgreement(userPublicKey).ToByteArrayUnsigned(); - var serverPublicKey = ((ECPublicKeyParameters) serverKeyPair.Public).Q.GetEncoded(false); - - var prk = HKDF(userSecret, key, Encoding.UTF8.GetBytes("Content-Encoding: auth\0"), 32); - var cek = HKDF(salt, prk, CreateInfoChunk("aesgcm", userKey, serverPublicKey), 16); - var nonce = HKDF(salt, prk, CreateInfoChunk("nonce", userKey, serverPublicKey), 12); - - var input = AddPaddingToInput(payload); - var encryptedMessage = EncryptAes(nonce, cek, input); - - return new EncryptionResult - { - Salt = salt, - Payload = encryptedMessage, - PublicKey = serverPublicKey - }; - } - - private static byte[] GenerateSalt(int length) - { - var salt = new byte[length]; - var random = new Random(); - random.NextBytes(salt); - return salt; - } - - private static byte[] AddPaddingToInput(byte[] data) - { - var input = new byte[0 + 2 + data.Length]; - Buffer.BlockCopy(ConvertInt(0), 0, input, 0, 2); - Buffer.BlockCopy(data, 0, input, 0 + 2, data.Length); - return input; - } - - private static byte[] EncryptAes(byte[] nonce, byte[] cek, byte[] message) - { - var cipher = new GcmBlockCipher(new AesEngine()); - var parameters = new AeadParameters(new KeyParameter(cek), 128, nonce); - cipher.Init(true, parameters); - - //Generate Cipher Text With Auth Tag - var cipherText = new byte[cipher.GetOutputSize(message.Length)]; - var len = cipher.ProcessBytes(message, 0, message.Length, cipherText, 0); - cipher.DoFinal(cipherText, len); - - //byte[] tag = cipher.GetMac(); - return cipherText; - } - - public static byte[] HKDFSecondStep(byte[] key, byte[] info, int length) - { - var hmac = new HmacSha256(key); - var infoAndOne = info.Concat(new byte[] {0x01}).ToArray(); - var result = hmac.ComputeHash(infoAndOne); - - if (result.Length > length) - { - Array.Resize(ref result, length); - } - - return result; - } - - public static byte[] HKDF(byte[] salt, byte[] prk, byte[] info, int length) - { - var hmac = new HmacSha256(salt); - var key = hmac.ComputeHash(prk); - - return HKDFSecondStep(key, info, length); - } - - public static byte[] ConvertInt(int number) - { - var output = BitConverter.GetBytes(Convert.ToUInt16(number)); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(output); - } - - return output; - } - - public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey) - { - var output = new List(); - output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}\0P-256\0")); - output.AddRange(ConvertInt(recipientPublicKey.Length)); - output.AddRange(recipientPublicKey); - output.AddRange(ConvertInt(senderPublicKey.Length)); - output.AddRange(senderPublicKey); - return output.ToArray(); - } - } - - public class HmacSha256 - { - private readonly HMac _hmac; - - public HmacSha256(byte[] key) - { - _hmac = new HMac(new Sha256Digest()); - _hmac.Init(new KeyParameter(key)); - } - - public byte[] ComputeHash(byte[] value) - { - var resBuf = new byte[_hmac.GetMacSize()]; - _hmac.BlockUpdate(value, 0, value.Length); - _hmac.DoFinal(resBuf, 0); - - return resBuf; - } - } + // @LogicSoftware + // Originally from https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/Encryptor.cs + public static class Encryptor + { + public static EncryptionResult Encrypt(string userKey, string userSecret, string payload) + { + byte[] userKeyBytes = UrlBase64.Decode(userKey); + byte[] userSecretBytes = UrlBase64.Decode(userSecret); + byte[] payloadBytes = Encoding.UTF8.GetBytes(payload); + + return Encrypt(userKeyBytes, userSecretBytes, payloadBytes); + } + + public static EncryptionResult Encrypt(byte[] userKey, byte[] userSecret, byte[] payload) + { + var salt = GenerateSalt(16); + + byte[] serverPublicKey = null; + byte[] key = null; + + var cgnKey = ImportCngKeyFromPublicKey(userKey); + using (ECDiffieHellmanCng alice = new ECDiffieHellmanCng(256)) + { + alice.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hmac; + alice.HashAlgorithm = CngAlgorithm.Sha256; + alice.HmacKey = userSecret; + + serverPublicKey = ImportPublicKeyFromCngKey(alice.PublicKey.ToByteArray()); + key = alice.DeriveKeyMaterial(cgnKey); + } + + var prk = HKDFSecondStep(key, Encoding.UTF8.GetBytes("Content-Encoding: auth\0"), 32); + byte[] cek = HKDF(salt, prk, CreateInfoChunk("aesgcm", userKey, serverPublicKey), 16); + byte[] nonce = HKDF(salt, prk, CreateInfoChunk("nonce", userKey, serverPublicKey), 12); + + var input = AddPaddingToInput(payload); + + var encryptedMessage = EncryptAes(nonce, cek, input); + + return new EncryptionResult + { + Salt = salt, + Payload = encryptedMessage, + PublicKey = serverPublicKey + }; + } + + private static CngKey ImportCngKeyFromPublicKey(byte[] userKey) + { + var keyType = new byte[] { 0x45, 0x43, 0x4B, 0x31 }; + var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; + + var keyImport = keyType.Concat(keyLength).Concat(userKey.Skip(1)).ToArray(); + + return CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob); + } + + private static byte[] ImportPublicKeyFromCngKey(byte[] cngKey) + { + var keyImport = (new byte[] { 0x04 }).Concat(cngKey.Skip(8)).ToArray(); + + return keyImport; + } + + private static byte[] GenerateSalt(int length) + { + var salt = new byte[length]; + var random = new Random(); + random.NextBytes(salt); + return salt; + } + + private static byte[] AddPaddingToInput(byte[] data) + { + var input = new byte[0 + 2 + data.Length]; + Buffer.BlockCopy(ConvertInt(0), 0, input, 0, 2); + Buffer.BlockCopy(data, 0, input, 0 + 2, data.Length); + return input; + } + + + private static byte[] EncryptAes(byte[] nonce, byte[] cek, byte[] message) + { + using (AuthenticatedAesCng aes = new AuthenticatedAesCng()) + { + aes.CngMode = CngChainingMode.Gcm; + + aes.Key = cek; + + aes.IV = nonce; + + using (MemoryStream ms = new MemoryStream()) + using (var encryptor = aes.CreateAuthenticatedEncryptor()) + using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + { + // Encrypt the secret message + cs.Write(message, 0, message.Length); + + // Finish the encryption and get the output authentication tag and ciphertext + cs.FlushFinalBlock(); + var ciphertext = ms.ToArray(); + + var tag = encryptor.GetTag(); + + return ciphertext.Concat(tag).ToArray(); + } + } + } + + public static byte[] HKDFSecondStep(byte[] key, byte[] info, int length) + { + var hmac = new HMACSHA256(key); + var infoAndOne = info.Concat(new byte[] { 0x01 }).ToArray(); + var result = hmac.ComputeHash(infoAndOne); + + if (result.Length > length) + { + Array.Resize(ref result, length); + } + + return result; + } + + public static byte[] HKDF(byte[] salt, byte[] prk, byte[] info, int length) + { + var hmac = new HMACSHA256(salt); + var key = hmac.ComputeHash(prk); + + return HKDFSecondStep(key, info, length); + } + + public static byte[] ConvertInt(int number) + { + var output = BitConverter.GetBytes(Convert.ToUInt16(number)); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(output); + } + + return output; + } + + public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey) + { + var output = new List(); + output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}\0P-256\0")); + output.AddRange(ConvertInt(recipientPublicKey.Length)); + output.AddRange(recipientPublicKey); + output.AddRange(ConvertInt(senderPublicKey.Length)); + output.AddRange(senderPublicKey); + return output.ToArray(); + } + } } \ No newline at end of file diff --git a/WebPush/Util/JwsSigner.cs b/WebPush/Util/JwsSigner.cs index f000ff2..980e060 100644 --- a/WebPush/Util/JwsSigner.cs +++ b/WebPush/Util/JwsSigner.cs @@ -1,21 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; using Newtonsoft.Json; -using Org.BouncyCastle.Crypto.Digests; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Crypto.Signers; +using System.Security.Cryptography; namespace WebPush.Util { - internal class JwsSigner + internal class JwsSigner { - private readonly ECPrivateKeyParameters _privateKey; + private readonly AsymmetricAlgorithm _privateKey; - public JwsSigner(ECPrivateKeyParameters privateKey) + public JwsSigner(AsymmetricAlgorithm privateKey) { - _privateKey = privateKey; + _privateKey = privateKey; } /// @@ -30,25 +27,18 @@ public string GenerateSignature(Dictionary header, Dictionary header, Dictionary payload) @@ -67,13 +57,12 @@ private static byte[] ByteArrayPadLeft(byte[] src, int size) return dst; } - private static byte[] Sha256Hash(byte[] message) - { - var sha256Digest = new Sha256Digest(); - sha256Digest.BlockUpdate(message, 0, message.Length); - var hash = new byte[sha256Digest.GetDigestSize()]; - sha256Digest.DoFinal(hash, 0); - return hash; - } - } -} \ No newline at end of file + private static byte[] Sha256Hash(byte[] message) + { + using (SHA256 sha256Hash = SHA256.Create()) + { + return sha256Hash.ComputeHash(message); + } + } + } +} diff --git a/WebPush/VapidHelper.cs b/WebPush/VapidHelper.cs index 56d1276..89ba4c4 100644 --- a/WebPush/VapidHelper.cs +++ b/WebPush/VapidHelper.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Org.BouncyCastle.Crypto.Parameters; using WebPush.Util; namespace WebPush @@ -14,11 +13,11 @@ public static VapidDetails GenerateVapidKeys() { var results = new VapidDetails(); - var keys = ECKeyHelper.GenerateKeys(); - var publicKey = ((ECPublicKeyParameters) keys.Public).Q.GetEncoded(false); - var privateKey = ((ECPrivateKeyParameters) keys.Private).D.ToByteArrayUnsigned(); + var keys = ECKeyHelper.GenerateKeys(); + var publicKey = keys.PublicKey; + var privateKey = keys.PrivateKey; - results.PublicKey = UrlBase64.Encode(publicKey); + results.PublicKey = UrlBase64.Encode(publicKey); results.PrivateKey = UrlBase64.Encode(ByteArrayPadLeft(privateKey, 32)); return results; @@ -42,7 +41,8 @@ public static Dictionary GetVapidHeaders(string audience, string ValidatePublicKey(publicKey); ValidatePrivateKey(privateKey); - var decodedPrivateKey = UrlBase64.Decode(privateKey); + var decodedPublicKey = UrlBase64.Decode(publicKey); + var decodedPrivateKey = UrlBase64.Decode(privateKey); if (expiration == -1) { @@ -50,7 +50,7 @@ public static Dictionary GetVapidHeaders(string audience, string } else { - ValidateExpiration(expiration); + ValidateExpiration(expiration); } @@ -58,17 +58,19 @@ public static Dictionary GetVapidHeaders(string audience, string var jwtPayload = new Dictionary {{"aud", audience}, {"exp", expiration}, {"sub", subject}}; - var signingKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); + using (var signingKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey)) + { - var signer = new JwsSigner(signingKey); - var token = signer.GenerateSignature(header, jwtPayload); + var signer = new JwsSigner(signingKey); + var token = signer.GenerateSignature(header, jwtPayload); - var results = new Dictionary - { - {"Authorization", "WebPush " + token}, {"Crypto-Key", "p256ecdsa=" + publicKey} - }; + var results = new Dictionary + { + {"Authorization", "WebPush " + token}, {"Crypto-Key", "p256ecdsa=" + publicKey} + }; - return results; + return results; + } } public static void ValidateAudience(string audience) diff --git a/WebPush/WebPush.csproj b/WebPush/WebPush.csproj index 016d102..0081736 100755 --- a/WebPush/WebPush.csproj +++ b/WebPush/WebPush.csproj @@ -1,7 +1,7 @@  - netstandard1.1;netstandard2.0;net45;net46 + net45;net46;net48; true 1.0.11 Cory Thompson @@ -24,8 +24,10 @@ - + + + + - diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs index 547d1e6..4b88cfa 100755 --- a/WebPush/WebPushClient.cs +++ b/WebPush/WebPushClient.cs @@ -376,7 +376,7 @@ private static void HandleResponse(HttpResponseMessage response, PushSubscriptio break; } - throw new WebPushException(message, response.StatusCode, response.Headers, subscription); + throw new WebPushException(message, response.StatusCode, response.Headers, subscription, response.ReasonPhrase); } public void Dispose() From 34ca270b615e4b311e9745ae4684bce27e5235e9 Mon Sep 17 00:00:00 2001 From: panico Date: Fri, 18 Dec 2020 15:09:41 +0100 Subject: [PATCH 2/7] replace tab by space --- WebPush.Test/ECKeyHelperTest.cs | 60 +++--- WebPush.Test/JWSSignerTest.cs | 4 +- WebPush/Model/AsymmetricKeyPair.cs | 10 +- WebPush/Model/WebPushException.cs | 13 +- WebPush/Util/ECKeyHelper.cs | 150 +++++++------- WebPush/Util/Encryptor.cs | 306 ++++++++++++++--------------- WebPush/Util/JwsSigner.cs | 42 ++-- WebPush/VapidHelper.cs | 38 ++-- WebPush/WebPush.csproj | 4 +- WebPush/WebPushClient.cs | 24 +-- 10 files changed, 322 insertions(+), 329 deletions(-) diff --git a/WebPush.Test/ECKeyHelperTest.cs b/WebPush.Test/ECKeyHelperTest.cs index 21bb27f..747d49c 100644 --- a/WebPush.Test/ECKeyHelperTest.cs +++ b/WebPush.Test/ECKeyHelperTest.cs @@ -16,11 +16,11 @@ public class ECKeyHelperTest [TestMethod] public void TestGenerateKeys() { - var keys = ECKeyHelper.GenerateKeys(); - var publicKey = keys.PublicKey; - var privateKey = keys.PrivateKey; + var keys = ECKeyHelper.GenerateKeys(); + var publicKey = keys.PublicKey; + var privateKey = keys.PrivateKey; - var publicKeyLength = publicKey.Length; + var publicKeyLength = publicKey.Length; var privateKeyLength = privateKey.Length; Assert.AreEqual(65, publicKeyLength); @@ -30,41 +30,41 @@ public void TestGenerateKeys() [TestMethod] public void TestGenerateKeysNoCache() { - var keys = ECKeyHelper.GenerateKeys(); - var publicKey1 = keys.PublicKey; - var privateKey1 = keys.PrivateKey; + var keys = ECKeyHelper.GenerateKeys(); + var publicKey1 = keys.PublicKey; + var privateKey1 = keys.PrivateKey; - var keys2 = ECKeyHelper.GenerateKeys(); - var publicKey2 = keys2.PublicKey; - var privateKey2 = keys2.PrivateKey; + var keys2 = ECKeyHelper.GenerateKeys(); + var publicKey2 = keys2.PublicKey; + var privateKey2 = keys2.PrivateKey; - Assert.IsFalse(publicKey1.SequenceEqual(publicKey2)); + Assert.IsFalse(publicKey1.SequenceEqual(publicKey2)); Assert.IsFalse(privateKey1.SequenceEqual(privateKey2)); } - [TestMethod] - public void TestGetPrivateKey() - { - #if NET48 - var privateKey = UrlBase64.Decode(TestPrivateKey); - var privateKeyParams = ECKeyHelper.GetPrivateKey(privateKey); + [TestMethod] + public void TestGetPrivateKey() + { +#if NET48 + var privateKey = UrlBase64.Decode(TestPrivateKey); + var privateKeyParams = ECKeyHelper.GetPrivateKey(privateKey); - var importedPrivateKey = UrlBase64.Encode((privateKeyParams as ECDsaCng).ExportParameters(true).D); + var importedPrivateKey = UrlBase64.Encode((privateKeyParams as ECDsaCng).ExportParameters(true).D); - Assert.AreEqual(TestPrivateKey, importedPrivateKey); - #endif - } + Assert.AreEqual(TestPrivateKey, importedPrivateKey); +#endif + } - [TestMethod] - public void TestGetPublicKey() - { - var publicKey = UrlBase64.Decode(TestPublicKey); - var publicKeyParams = ECKeyHelper.GetPublicKey(publicKey); + [TestMethod] + public void TestGetPublicKey() + { + var publicKey = UrlBase64.Decode(TestPublicKey); + var publicKeyParams = ECKeyHelper.GetPublicKey(publicKey); - var importedPublicKey = UrlBase64.Encode(publicKeyParams.GetECPublicKey()); + var importedPublicKey = UrlBase64.Encode(publicKeyParams.GetECPublicKey()); - Assert.AreEqual(TestPublicKey, importedPublicKey); - } - } + Assert.AreEqual(TestPublicKey, importedPublicKey); + } + } } \ No newline at end of file diff --git a/WebPush.Test/JWSSignerTest.cs b/WebPush.Test/JWSSignerTest.cs index f489280..184d755 100644 --- a/WebPush.Test/JWSSignerTest.cs +++ b/WebPush.Test/JWSSignerTest.cs @@ -14,9 +14,9 @@ public class JWSSignerTest public void TestGenerateSignature() { var decodedPrivateKey = UrlBase64.Decode(TestPrivateKey); - var privateKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); + var privateKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); - var header = new Dictionary(); + var header = new Dictionary(); header.Add("typ", "JWT"); header.Add("alg", "ES256"); diff --git a/WebPush/Model/AsymmetricKeyPair.cs b/WebPush/Model/AsymmetricKeyPair.cs index 47bfcd9..a9049a8 100644 --- a/WebPush/Model/AsymmetricKeyPair.cs +++ b/WebPush/Model/AsymmetricKeyPair.cs @@ -6,9 +6,9 @@ namespace WebPush { - public class AsymmetricKeyPair - { - public byte[] PublicKey; - public byte[] PrivateKey; - } + public class AsymmetricKeyPair + { + public byte[] PublicKey; + public byte[] PrivateKey; + } } diff --git a/WebPush/Model/WebPushException.cs b/WebPush/Model/WebPushException.cs index ddfa795..80a893f 100644 --- a/WebPush/Model/WebPushException.cs +++ b/WebPush/Model/WebPushException.cs @@ -14,17 +14,8 @@ public WebPushException(string message, HttpStatusCode statusCode, HttpResponseH PushSubscription = pushSubscription; } - public WebPushException(string message, HttpStatusCode statusCode, HttpResponseHeaders headers, - PushSubscription pushSubscription,string reasonPhrase) : this( - message,statusCode,headers,pushSubscription) - { - ReasonPhrase = reasonPhrase; - } - - public HttpStatusCode StatusCode { get; set; } + public HttpStatusCode StatusCode { get; set; } public HttpResponseHeaders Headers { get; set; } public PushSubscription PushSubscription { get; set; } - public string ReasonPhrase { get; set; } - - } + } } \ No newline at end of file diff --git a/WebPush/Util/ECKeyHelper.cs b/WebPush/Util/ECKeyHelper.cs index e3d2fa6..7ba2aed 100644 --- a/WebPush/Util/ECKeyHelper.cs +++ b/WebPush/Util/ECKeyHelper.cs @@ -3,28 +3,29 @@ namespace WebPush.Util { - internal static class ECKeyHelper - { - public static byte[] GetECPublicKey(this CngKey key){ - var cngKey = key.Export(CngKeyBlobFormat.EccPublicBlob); - return new byte[] { 0x04 }.Concat(cngKey.Skip(8)).ToArray(); - } + internal static class ECKeyHelper + { + public static byte[] GetECPublicKey(this CngKey key) + { + var cngKey = key.Export(CngKeyBlobFormat.EccPublicBlob); + return new byte[] { 0x04 }.Concat(cngKey.Skip(8)).ToArray(); + } - public static byte[] GetECPrivateKey(this CngKey key) - { - var cngKey = key.Export(CngKeyBlobFormat.EccPrivateBlob); - return cngKey.Skip(8 + 32 + 32).Take(32).ToArray(); - } + public static byte[] GetECPrivateKey(this CngKey key) + { + var cngKey = key.Export(CngKeyBlobFormat.EccPrivateBlob); + return cngKey.Skip(8 + 32 + 32).Take(32).ToArray(); + } - public static CngKey GetPublicKey(byte[] key) - { - var keyType = new byte[] { 0x45, 0x43, 0x4B, 0x31 }; - var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; + public static CngKey GetPublicKey(byte[] key) + { + var keyType = new byte[] { 0x45, 0x43, 0x4B, 0x31 }; + var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; - var keyImport = keyType.Concat(keyLength).Concat(key.Skip(1)).ToArray(); + var keyImport = keyType.Concat(keyLength).Concat(key.Skip(1)).ToArray(); - return CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob); - } + return CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob); + } #if NET48 public static AsymmetricAlgorithm GetPrivateKey(byte[] privateKey) { @@ -48,65 +49,66 @@ public static AsymmetricKeyPair GenerateKeys() } } #else - private static CngKey ImportPrivCngKey(byte[] pubKey, byte[] privKey) - { - // to import keys to CngKey in ECCPublicKeyBlob and ECCPrivateKeyBlob format, keys should be form in specific formats as noted here : - // https://stackoverflow.com/a/24255090 - // magic prefixes : https://referencesource.microsoft.com/#system.core/System/Security/Cryptography/BCryptNative.cs,fde0749a0a5f70d8,references - var keyType = new byte[] { 0x45, 0x43, 0x53, 0x32 }; - var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; + private static CngKey ImportPrivCngKey(byte[] pubKey, byte[] privKey) + { + // to import keys to CngKey in ECCPublicKeyBlob and ECCPrivateKeyBlob format, keys should be form in specific formats as noted here : + // https://stackoverflow.com/a/24255090 + // magic prefixes : https://referencesource.microsoft.com/#system.core/System/Security/Cryptography/BCryptNative.cs,fde0749a0a5f70d8,references + var keyType = new byte[] { 0x45, 0x43, 0x53, 0x32 }; + var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; - var key = pubKey.Skip(1); + var key = pubKey.Skip(1); - var keyImport = keyType.Concat(keyLength).Concat(key).Concat(privKey).ToArray(); + var keyImport = keyType.Concat(keyLength).Concat(key).Concat(privKey).ToArray(); - var cngKey = CngKey.Import(keyImport, CngKeyBlobFormat.EccPrivateBlob); - return cngKey; - } - public static ECDsaCng GetPrivateKey(byte[] privateKey) - { - var fakePubKey = new byte[64]; - var publicKey = (new byte[] { 0x04 }).Concat(fakePubKey).ToArray(); + var cngKey = CngKey.Import(keyImport, CngKeyBlobFormat.EccPrivateBlob); + return cngKey; + } + public static ECDsaCng GetPrivateKey(byte[] privateKey) + { + var fakePubKey = new byte[64]; + var publicKey = (new byte[] { 0x04 }).Concat(fakePubKey).ToArray(); - var cngKey = ImportPrivCngKey(publicKey, privateKey); - var ecDsaCng = new ECDsaCng(cngKey); - ecDsaCng.HashAlgorithm = CngAlgorithm.ECDsaP256; - return ecDsaCng; - } - - public static AsymmetricKeyPair GenerateKeys() - { - CngProvider cp = CngProvider.MicrosoftSoftwareKeyStorageProvider; - string keyName = "tempvapidkey"; - if (CngKey.Exists(keyName, cp)) - { - using (CngKey cngKey = CngKey.Open(keyName, cp)) - cngKey.Delete(); - } - CngKeyCreationParameters kcp = new CngKeyCreationParameters - { - Provider = cp, - ExportPolicy = CngExportPolicies.AllowPlaintextExport - }; - try - { - using (CngKey myKey = CngKey.Create(CngAlgorithm.ECDiffieHellmanP256, keyName, kcp)) - { - return new AsymmetricKeyPair() - { - PublicKey = myKey.GetECPublicKey(), - PrivateKey = myKey.GetECPrivateKey() - }; - } - } - finally{ - if (CngKey.Exists(keyName, cp)) - { - using (CngKey cngKey = CngKey.Open(keyName, cp)) - cngKey.Delete(); - } - } - } + var cngKey = ImportPrivCngKey(publicKey, privateKey); + var ecDsaCng = new ECDsaCng(cngKey); + ecDsaCng.HashAlgorithm = CngAlgorithm.ECDsaP256; + return ecDsaCng; + } + + public static AsymmetricKeyPair GenerateKeys() + { + CngProvider cp = CngProvider.MicrosoftSoftwareKeyStorageProvider; + string keyName = "tempvapidkey"; + if (CngKey.Exists(keyName, cp)) + { + using (CngKey cngKey = CngKey.Open(keyName, cp)) + cngKey.Delete(); + } + CngKeyCreationParameters kcp = new CngKeyCreationParameters + { + Provider = cp, + ExportPolicy = CngExportPolicies.AllowPlaintextExport + }; + try + { + using (CngKey myKey = CngKey.Create(CngAlgorithm.ECDiffieHellmanP256, keyName, kcp)) + { + return new AsymmetricKeyPair() + { + PublicKey = myKey.GetECPublicKey(), + PrivateKey = myKey.GetECPrivateKey() + }; + } + } + finally + { + if (CngKey.Exists(keyName, cp)) + { + using (CngKey cngKey = CngKey.Open(keyName, cp)) + cngKey.Delete(); + } + } + } #endif - } + } } \ No newline at end of file diff --git a/WebPush/Util/Encryptor.cs b/WebPush/Util/Encryptor.cs index ffce696..59957ce 100644 --- a/WebPush/Util/Encryptor.cs +++ b/WebPush/Util/Encryptor.cs @@ -8,157 +8,157 @@ namespace WebPush.Util { - // @LogicSoftware - // Originally from https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/Encryptor.cs - public static class Encryptor - { - public static EncryptionResult Encrypt(string userKey, string userSecret, string payload) - { - byte[] userKeyBytes = UrlBase64.Decode(userKey); - byte[] userSecretBytes = UrlBase64.Decode(userSecret); - byte[] payloadBytes = Encoding.UTF8.GetBytes(payload); - - return Encrypt(userKeyBytes, userSecretBytes, payloadBytes); - } - - public static EncryptionResult Encrypt(byte[] userKey, byte[] userSecret, byte[] payload) - { - var salt = GenerateSalt(16); - - byte[] serverPublicKey = null; - byte[] key = null; - - var cgnKey = ImportCngKeyFromPublicKey(userKey); - using (ECDiffieHellmanCng alice = new ECDiffieHellmanCng(256)) - { - alice.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hmac; - alice.HashAlgorithm = CngAlgorithm.Sha256; - alice.HmacKey = userSecret; - - serverPublicKey = ImportPublicKeyFromCngKey(alice.PublicKey.ToByteArray()); - key = alice.DeriveKeyMaterial(cgnKey); - } - - var prk = HKDFSecondStep(key, Encoding.UTF8.GetBytes("Content-Encoding: auth\0"), 32); - byte[] cek = HKDF(salt, prk, CreateInfoChunk("aesgcm", userKey, serverPublicKey), 16); - byte[] nonce = HKDF(salt, prk, CreateInfoChunk("nonce", userKey, serverPublicKey), 12); - - var input = AddPaddingToInput(payload); - - var encryptedMessage = EncryptAes(nonce, cek, input); - - return new EncryptionResult - { - Salt = salt, - Payload = encryptedMessage, - PublicKey = serverPublicKey - }; - } - - private static CngKey ImportCngKeyFromPublicKey(byte[] userKey) - { - var keyType = new byte[] { 0x45, 0x43, 0x4B, 0x31 }; - var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; - - var keyImport = keyType.Concat(keyLength).Concat(userKey.Skip(1)).ToArray(); - - return CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob); - } - - private static byte[] ImportPublicKeyFromCngKey(byte[] cngKey) - { - var keyImport = (new byte[] { 0x04 }).Concat(cngKey.Skip(8)).ToArray(); - - return keyImport; - } - - private static byte[] GenerateSalt(int length) - { - var salt = new byte[length]; - var random = new Random(); - random.NextBytes(salt); - return salt; - } - - private static byte[] AddPaddingToInput(byte[] data) - { - var input = new byte[0 + 2 + data.Length]; - Buffer.BlockCopy(ConvertInt(0), 0, input, 0, 2); - Buffer.BlockCopy(data, 0, input, 0 + 2, data.Length); - return input; - } - - - private static byte[] EncryptAes(byte[] nonce, byte[] cek, byte[] message) - { - using (AuthenticatedAesCng aes = new AuthenticatedAesCng()) - { - aes.CngMode = CngChainingMode.Gcm; - - aes.Key = cek; - - aes.IV = nonce; - - using (MemoryStream ms = new MemoryStream()) - using (var encryptor = aes.CreateAuthenticatedEncryptor()) - using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) - { - // Encrypt the secret message - cs.Write(message, 0, message.Length); - - // Finish the encryption and get the output authentication tag and ciphertext - cs.FlushFinalBlock(); - var ciphertext = ms.ToArray(); - - var tag = encryptor.GetTag(); - - return ciphertext.Concat(tag).ToArray(); - } - } - } - - public static byte[] HKDFSecondStep(byte[] key, byte[] info, int length) - { - var hmac = new HMACSHA256(key); - var infoAndOne = info.Concat(new byte[] { 0x01 }).ToArray(); - var result = hmac.ComputeHash(infoAndOne); - - if (result.Length > length) - { - Array.Resize(ref result, length); - } - - return result; - } - - public static byte[] HKDF(byte[] salt, byte[] prk, byte[] info, int length) - { - var hmac = new HMACSHA256(salt); - var key = hmac.ComputeHash(prk); - - return HKDFSecondStep(key, info, length); - } - - public static byte[] ConvertInt(int number) - { - var output = BitConverter.GetBytes(Convert.ToUInt16(number)); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(output); - } - - return output; - } - - public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey) - { - var output = new List(); - output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}\0P-256\0")); - output.AddRange(ConvertInt(recipientPublicKey.Length)); - output.AddRange(recipientPublicKey); - output.AddRange(ConvertInt(senderPublicKey.Length)); - output.AddRange(senderPublicKey); - return output.ToArray(); - } - } + // @LogicSoftware + // Originally from https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/Encryptor.cs + public static class Encryptor + { + public static EncryptionResult Encrypt(string userKey, string userSecret, string payload) + { + byte[] userKeyBytes = UrlBase64.Decode(userKey); + byte[] userSecretBytes = UrlBase64.Decode(userSecret); + byte[] payloadBytes = Encoding.UTF8.GetBytes(payload); + + return Encrypt(userKeyBytes, userSecretBytes, payloadBytes); + } + + public static EncryptionResult Encrypt(byte[] userKey, byte[] userSecret, byte[] payload) + { + var salt = GenerateSalt(16); + + byte[] serverPublicKey = null; + byte[] key = null; + + var cgnKey = ImportCngKeyFromPublicKey(userKey); + using (ECDiffieHellmanCng alice = new ECDiffieHellmanCng(256)) + { + alice.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hmac; + alice.HashAlgorithm = CngAlgorithm.Sha256; + alice.HmacKey = userSecret; + + serverPublicKey = ImportPublicKeyFromCngKey(alice.PublicKey.ToByteArray()); + key = alice.DeriveKeyMaterial(cgnKey); + } + + var prk = HKDFSecondStep(key, Encoding.UTF8.GetBytes("Content-Encoding: auth\0"), 32); + byte[] cek = HKDF(salt, prk, CreateInfoChunk("aesgcm", userKey, serverPublicKey), 16); + byte[] nonce = HKDF(salt, prk, CreateInfoChunk("nonce", userKey, serverPublicKey), 12); + + var input = AddPaddingToInput(payload); + + var encryptedMessage = EncryptAes(nonce, cek, input); + + return new EncryptionResult + { + Salt = salt, + Payload = encryptedMessage, + PublicKey = serverPublicKey + }; + } + + private static CngKey ImportCngKeyFromPublicKey(byte[] userKey) + { + var keyType = new byte[] { 0x45, 0x43, 0x4B, 0x31 }; + var keyLength = new byte[] { 0x20, 0x00, 0x00, 0x00 }; + + var keyImport = keyType.Concat(keyLength).Concat(userKey.Skip(1)).ToArray(); + + return CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob); + } + + private static byte[] ImportPublicKeyFromCngKey(byte[] cngKey) + { + var keyImport = (new byte[] { 0x04 }).Concat(cngKey.Skip(8)).ToArray(); + + return keyImport; + } + + private static byte[] GenerateSalt(int length) + { + var salt = new byte[length]; + var random = new Random(); + random.NextBytes(salt); + return salt; + } + + private static byte[] AddPaddingToInput(byte[] data) + { + var input = new byte[0 + 2 + data.Length]; + Buffer.BlockCopy(ConvertInt(0), 0, input, 0, 2); + Buffer.BlockCopy(data, 0, input, 0 + 2, data.Length); + return input; + } + + + private static byte[] EncryptAes(byte[] nonce, byte[] cek, byte[] message) + { + using (AuthenticatedAesCng aes = new AuthenticatedAesCng()) + { + aes.CngMode = CngChainingMode.Gcm; + + aes.Key = cek; + + aes.IV = nonce; + + using (MemoryStream ms = new MemoryStream()) + using (var encryptor = aes.CreateAuthenticatedEncryptor()) + using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + { + // Encrypt the secret message + cs.Write(message, 0, message.Length); + + // Finish the encryption and get the output authentication tag and ciphertext + cs.FlushFinalBlock(); + var ciphertext = ms.ToArray(); + + var tag = encryptor.GetTag(); + + return ciphertext.Concat(tag).ToArray(); + } + } + } + + public static byte[] HKDFSecondStep(byte[] key, byte[] info, int length) + { + var hmac = new HMACSHA256(key); + var infoAndOne = info.Concat(new byte[] { 0x01 }).ToArray(); + var result = hmac.ComputeHash(infoAndOne); + + if (result.Length > length) + { + Array.Resize(ref result, length); + } + + return result; + } + + public static byte[] HKDF(byte[] salt, byte[] prk, byte[] info, int length) + { + var hmac = new HMACSHA256(salt); + var key = hmac.ComputeHash(prk); + + return HKDFSecondStep(key, info, length); + } + + public static byte[] ConvertInt(int number) + { + var output = BitConverter.GetBytes(Convert.ToUInt16(number)); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(output); + } + + return output; + } + + public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey) + { + var output = new List(); + output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}\0P-256\0")); + output.AddRange(ConvertInt(recipientPublicKey.Length)); + output.AddRange(recipientPublicKey); + output.AddRange(ConvertInt(senderPublicKey.Length)); + output.AddRange(senderPublicKey); + return output.ToArray(); + } + } } \ No newline at end of file diff --git a/WebPush/Util/JwsSigner.cs b/WebPush/Util/JwsSigner.cs index 980e060..a165e8c 100644 --- a/WebPush/Util/JwsSigner.cs +++ b/WebPush/Util/JwsSigner.cs @@ -6,13 +6,13 @@ namespace WebPush.Util { - internal class JwsSigner + internal class JwsSigner { private readonly AsymmetricAlgorithm _privateKey; public JwsSigner(AsymmetricAlgorithm privateKey) { - _privateKey = privateKey; + _privateKey = privateKey; } /// @@ -27,18 +27,18 @@ public string GenerateSignature(Dictionary header, Dictionary header, Dictionary payload) @@ -57,12 +57,12 @@ private static byte[] ByteArrayPadLeft(byte[] src, int size) return dst; } - private static byte[] Sha256Hash(byte[] message) - { - using (SHA256 sha256Hash = SHA256.Create()) - { - return sha256Hash.ComputeHash(message); - } - } - } -} + private static byte[] Sha256Hash(byte[] message) + { + using (SHA256 sha256Hash = SHA256.Create()) + { + return sha256Hash.ComputeHash(message); + } + } + } +} \ No newline at end of file diff --git a/WebPush/VapidHelper.cs b/WebPush/VapidHelper.cs index 89ba4c4..70edba2 100644 --- a/WebPush/VapidHelper.cs +++ b/WebPush/VapidHelper.cs @@ -13,11 +13,11 @@ public static VapidDetails GenerateVapidKeys() { var results = new VapidDetails(); - var keys = ECKeyHelper.GenerateKeys(); - var publicKey = keys.PublicKey; - var privateKey = keys.PrivateKey; + var keys = ECKeyHelper.GenerateKeys(); + var publicKey = keys.PublicKey; + var privateKey = keys.PrivateKey; - results.PublicKey = UrlBase64.Encode(publicKey); + results.PublicKey = UrlBase64.Encode(publicKey); results.PrivateKey = UrlBase64.Encode(ByteArrayPadLeft(privateKey, 32)); return results; @@ -41,8 +41,8 @@ public static Dictionary GetVapidHeaders(string audience, string ValidatePublicKey(publicKey); ValidatePrivateKey(privateKey); - var decodedPublicKey = UrlBase64.Decode(publicKey); - var decodedPrivateKey = UrlBase64.Decode(privateKey); + var decodedPublicKey = UrlBase64.Decode(publicKey); + var decodedPrivateKey = UrlBase64.Decode(privateKey); if (expiration == -1) { @@ -54,23 +54,23 @@ public static Dictionary GetVapidHeaders(string audience, string } - var header = new Dictionary {{"typ", "JWT"}, {"alg", "ES256"}}; + var header = new Dictionary { { "typ", "JWT" }, { "alg", "ES256" } }; - var jwtPayload = new Dictionary {{"aud", audience}, {"exp", expiration}, {"sub", subject}}; + var jwtPayload = new Dictionary { { "aud", audience }, { "exp", expiration }, { "sub", subject } }; - using (var signingKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey)) - { + using (var signingKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey)) + { - var signer = new JwsSigner(signingKey); - var token = signer.GenerateSignature(header, jwtPayload); + var signer = new JwsSigner(signingKey); + var token = signer.GenerateSignature(header, jwtPayload); - var results = new Dictionary - { - {"Authorization", "WebPush " + token}, {"Crypto-Key", "p256ecdsa=" + publicKey} - }; + var results = new Dictionary + { + {"Authorization", "WebPush " + token}, {"Crypto-Key", "p256ecdsa=" + publicKey} + }; - return results; - } + return results; + } } public static void ValidateAudience(string audience) @@ -154,7 +154,7 @@ private static void ValidateExpiration(long expiration) private static long UnixTimeNow() { var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0); - return (long) timeSpan.TotalSeconds; + return (long)timeSpan.TotalSeconds; } private static byte[] ByteArrayPadLeft(byte[] src, int size) diff --git a/WebPush/WebPush.csproj b/WebPush/WebPush.csproj index 0081736..341baf8 100755 --- a/WebPush/WebPush.csproj +++ b/WebPush/WebPush.csproj @@ -25,9 +25,9 @@ - + - + \ No newline at end of file diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs index 4b88cfa..c57306e 100755 --- a/WebPush/WebPushClient.cs +++ b/WebPush/WebPushClient.cs @@ -26,14 +26,14 @@ public class WebPushClient : IDisposable public WebPushClient() { - + } public WebPushClient(HttpClient httpClient) { _httpClient = httpClient; } - + public WebPushClient(HttpClientHandler httpClientHandler) { _httpClientHandler = httpClientHandler; @@ -142,7 +142,7 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, if (options != null) { - var validOptionsKeys = new List {"headers", "gcmAPIKey", "vapidDetails", "TTL"}; + var validOptionsKeys = new List { "headers", "gcmAPIKey", "vapidDetails", "TTL" }; foreach (var key in options.Keys) { if (!validOptionsKeys.Contains(key)) @@ -181,7 +181,7 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, } //at this stage ttl cannot be null. - timeToLive = (int) ttl; + timeToLive = (int)ttl; } } @@ -277,7 +277,7 @@ public void SendNotification(PushSubscription subscription, string payload = nul /// The vapid details for the notification. public void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails) { - var options = new Dictionary {["vapidDetails"] = vapidDetails}; + var options = new Dictionary { ["vapidDetails"] = vapidDetails }; SendNotification(subscription, payload, options); } @@ -290,7 +290,7 @@ public void SendNotification(PushSubscription subscription, string payload, Vapi /// The GCM API key public void SendNotification(PushSubscription subscription, string payload, string gcmApiKey) { - var options = new Dictionary {["gcmAPIKey"] = gcmApiKey}; + var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey }; SendNotification(subscription, payload, options); } @@ -323,7 +323,7 @@ public async Task SendNotificationAsync(PushSubscription subscription, string pa public async Task SendNotificationAsync(PushSubscription subscription, string payload, VapidDetails vapidDetails) { - var options = new Dictionary {["vapidDetails"] = vapidDetails}; + var options = new Dictionary { ["vapidDetails"] = vapidDetails }; await SendNotificationAsync(subscription, payload, options); } @@ -336,7 +336,7 @@ public async Task SendNotificationAsync(PushSubscription subscription, string pa /// The GCM API key public async Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey) { - var options = new Dictionary {["gcmAPIKey"] = gcmApiKey}; + var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey }; await SendNotificationAsync(subscription, payload, options); } @@ -355,7 +355,7 @@ private static void HandleResponse(HttpResponseMessage response, PushSubscriptio } // Error - var message = @"Received unexpected response code: " + (int) response.StatusCode; + var message = @"Received unexpected response code: " + (int)response.StatusCode; switch (response.StatusCode) { case HttpStatusCode.BadRequest: @@ -366,7 +366,7 @@ private static void HandleResponse(HttpResponseMessage response, PushSubscriptio message = "Payload too large"; break; - case (HttpStatusCode) 429: + case (HttpStatusCode)429: message = "Too many request."; break; @@ -376,9 +376,9 @@ private static void HandleResponse(HttpResponseMessage response, PushSubscriptio break; } - throw new WebPushException(message, response.StatusCode, response.Headers, subscription, response.ReasonPhrase); + throw new WebPushException(message, response.StatusCode, response.Headers, subscription); } - + public void Dispose() { if (_httpClient != null && _isHttpClientInternallyCreated) From fe1b2a6eedd61ac8f7004137959fbc08cb495eeb Mon Sep 17 00:00:00 2001 From: panico Date: Fri, 18 Dec 2020 21:48:06 +0100 Subject: [PATCH 3/7] Add .netcore3.0 support --- .travis.yml | 4 ++-- WebPush.Test/WebPush.Test.csproj | 8 +++++++- WebPush/WebPush.csproj | 7 ++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index fd30ea6..293deb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: csharp dist: trusty mono: none -dotnet: 2.0.0 +dotnet: 3.0.0 install: - dotnet restore script: - - dotnet test --framework=netcoreapp2.0 WebPush.Test/WebPush.Test.csproj \ No newline at end of file + - dotnet test --framework=netcoreapp3.0 WebPush.Test/WebPush.Test.csproj \ No newline at end of file diff --git a/WebPush.Test/WebPush.Test.csproj b/WebPush.Test/WebPush.Test.csproj index 0b7328c..7624825 100755 --- a/WebPush.Test/WebPush.Test.csproj +++ b/WebPush.Test/WebPush.Test.csproj @@ -1,7 +1,7 @@  - net45;net46;net48; + net45;net46;net48;netcoreapp3.0 false @@ -17,4 +17,10 @@ + + + 5.0.0 + + + diff --git a/WebPush/WebPush.csproj b/WebPush/WebPush.csproj index 341baf8..6db90d8 100755 --- a/WebPush/WebPush.csproj +++ b/WebPush/WebPush.csproj @@ -1,7 +1,7 @@  - net45;net46;net48; + net45;net46;net48;netcoreapp3.0; true 1.0.11 Cory Thompson @@ -30,4 +30,9 @@ + + + 5.0.0 + + \ No newline at end of file From 0f63251e37ca82afd033f14d39d4f0f8d62bd7bb Mon Sep 17 00:00:00 2001 From: nanspanico <75785820+nanspanico@users.noreply.github.com> Date: Sun, 20 Dec 2020 01:16:04 +0100 Subject: [PATCH 4/7] Update .travis.yml change dotnet version --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 293deb4..f091069 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: csharp dist: trusty mono: none -dotnet: 3.0.0 +dotnet: 3.0.100 install: - dotnet restore script: - - dotnet test --framework=netcoreapp3.0 WebPush.Test/WebPush.Test.csproj \ No newline at end of file + - dotnet test --framework=netcoreapp3.0 WebPush.Test/WebPush.Test.csproj From 5e911554b26bf040f345730ffad7c0138f9f6826 Mon Sep 17 00:00:00 2001 From: nanspanico <75785820+nanspanico@users.noreply.github.com> Date: Sun, 20 Dec 2020 01:20:45 +0100 Subject: [PATCH 5/7] Update .travis.yml fix .net version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f091069..cffff36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: csharp dist: trusty mono: none -dotnet: 3.0.100 +dotnet: 3.1.401 install: - dotnet restore From a6d5f2172f68a2de083634ed2555e1b4762364c1 Mon Sep 17 00:00:00 2001 From: nanspanico <75785820+nanspanico@users.noreply.github.com> Date: Sun, 20 Dec 2020 01:30:37 +0100 Subject: [PATCH 6/7] Update .travis.yml fix .net version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cffff36..7e5b8d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: csharp dist: trusty mono: none -dotnet: 3.1.401 +dotnet: 3.1 install: - dotnet restore From da71c6d3bcd943dbbb23c5a48fc069786ff90794 Mon Sep 17 00:00:00 2001 From: panico Date: Wed, 6 Jan 2021 21:24:00 +0100 Subject: [PATCH 7/7] GenerateKeys with Ephemeral CNG Key --- WebPush/Util/ECKeyHelper.cs | 78 +++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/WebPush/Util/ECKeyHelper.cs b/WebPush/Util/ECKeyHelper.cs index 7ba2aed..91218a9 100644 --- a/WebPush/Util/ECKeyHelper.cs +++ b/WebPush/Util/ECKeyHelper.cs @@ -27,27 +27,27 @@ public static CngKey GetPublicKey(byte[] key) return CngKey.Import(keyImport, CngKeyBlobFormat.EccPublicBlob); } #if NET48 - public static AsymmetricAlgorithm GetPrivateKey(byte[] privateKey) - { - return ECDsaCng.Create(new ECParameters - { - Curve = ECCurve.NamedCurves.nistP256, - D = privateKey, - Q = new ECPoint(){ X = new byte[32],Y = new byte[32]} - }); - } - public static AsymmetricKeyPair GenerateKeys() - { + public static AsymmetricAlgorithm GetPrivateKey(byte[] privateKey) + { + return ECDsaCng.Create(new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + D = privateKey, + Q = new ECPoint(){ X = new byte[32],Y = new byte[32]} + }); + } + public static AsymmetricKeyPair GenerateKeys() + { - using (var cng = new ECDiffieHellmanCng(ECCurve.NamedCurves.nistP256)) - { - cng.GenerateKey(ECCurve.NamedCurves.nistP256); - var parameters = cng.ExportParameters(true); - var pr = parameters.D.ToArray(); - var pub = new byte[] { 0x04 }.Concat(parameters.Q.X).Concat(parameters.Q.Y).ToArray(); - return new AsymmetricKeyPair() { PublicKey = pub,PrivateKey = pr }; - } - } + using (var cng = new ECDiffieHellmanCng(ECCurve.NamedCurves.nistP256)) + { + cng.GenerateKey(ECCurve.NamedCurves.nistP256); + var parameters = cng.ExportParameters(true); + var pr = parameters.D.ToArray(); + var pub = new byte[] { 0x04 }.Concat(parameters.Q.X).Concat(parameters.Q.Y).ToArray(); + return new AsymmetricKeyPair() { PublicKey = pub,PrivateKey = pr }; + } + } #else private static CngKey ImportPrivCngKey(byte[] pubKey, byte[] privKey) { @@ -75,23 +75,35 @@ public static ECDsaCng GetPrivateKey(byte[] privateKey) return ecDsaCng; } - public static AsymmetricKeyPair GenerateKeys() + public static bool CngKeyExists(string keyName, CngProvider cp) { - CngProvider cp = CngProvider.MicrosoftSoftwareKeyStorageProvider; - string keyName = "tempvapidkey"; - if (CngKey.Exists(keyName, cp)) + if (string.IsNullOrEmpty(keyName)) + return false; + try { - using (CngKey cngKey = CngKey.Open(keyName, cp)) - cngKey.Delete(); + return CngKey.Exists(keyName, cp); } + catch (CryptographicException) { } + return false; + } + + public static AsymmetricKeyPair GenerateKeys() + { + //CngProvider cp = CngProvider.MicrosoftSoftwareKeyStorageProvider; + //string keyName = "tempvapidkey"; + //if (CngKeyExists(keyName, cp)) + //{ + // using (CngKey cngKey = CngKey.Open(keyName, cp)) + // cngKey.Delete(); + //} CngKeyCreationParameters kcp = new CngKeyCreationParameters { - Provider = cp, + //Provider = cp, ExportPolicy = CngExportPolicies.AllowPlaintextExport }; try { - using (CngKey myKey = CngKey.Create(CngAlgorithm.ECDiffieHellmanP256, keyName, kcp)) + using (CngKey myKey = CngKey.Create(CngAlgorithm.ECDiffieHellmanP256, null, kcp)) { return new AsymmetricKeyPair() { @@ -102,11 +114,11 @@ public static AsymmetricKeyPair GenerateKeys() } finally { - if (CngKey.Exists(keyName, cp)) - { - using (CngKey cngKey = CngKey.Open(keyName, cp)) - cngKey.Delete(); - } + //if (CngKeyExists(keyName, cp)) + //{ + // using (CngKey cngKey = CngKey.Open(keyName, cp)) + // cngKey.Delete(); + //} } } #endif