diff --git a/.gitignore b/.gitignore index 78f05bc..b9757b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin obj +artifacts *.xslt.sql *.user *.suo diff --git a/Directory.Build.props b/Directory.Build.props index f479e98..2267414 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,7 @@ true + true diff --git a/WebPush.Test/ECKeyHelperTest.cs b/WebPush.Test/ECKeyHelperTest.cs index ae2ef6d..cd03cec 100644 --- a/WebPush.Test/ECKeyHelperTest.cs +++ b/WebPush.Test/ECKeyHelperTest.cs @@ -1,69 +1,88 @@ using System.Linq; +using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Org.BouncyCastle.Crypto.Parameters; using WebPush.Util; -namespace WebPush.Test +namespace WebPush.Test; + +[TestClass] +public class ECKeyHelperTest { - [TestClass] - public class ECKeyHelperTest + private const string TestPublicKey = + @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; + + private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; + + [TestMethod] + public void TestGenerateKeys() { - private const string TestPublicKey = - @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; + var keypair = ECKeyHelper.GenerateKeys(); + var publicKey = keypair.GetPublicKey(); + var privateKey = keypair.GetPrivateKey(); - private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; + var publicKeyLength = publicKey.Length; + var privateKeyLength = privateKey.Length; - [TestMethod] - public void TestGenerateKeys() - { - var keys = ECKeyHelper.GenerateKeys(); + Assert.AreEqual(65, publicKeyLength); + Assert.AreEqual(32, privateKeyLength); + } + + [TestMethod] + public void TestGenerateKeysNoCache() + { + var keys1 = ECKeyHelper.GenerateKeys(); + var keys2 = ECKeyHelper.GenerateKeys(); - var publicKey = ((ECPublicKeyParameters) keys.Public).Q.GetEncoded(false); - var privateKey = ((ECPrivateKeyParameters) keys.Private).D.ToByteArrayUnsigned(); + var publicKey1 = keys1.GetPublicKey(); + var privateKey1 = keys1.GetPrivateKey(); - var publicKeyLength = publicKey.Length; - var privateKeyLength = privateKey.Length; + var publicKey2 = keys2.GetPublicKey(); + var privateKey2 = keys2.GetPrivateKey(); - Assert.AreEqual(65, publicKeyLength); - Assert.AreEqual(32, privateKeyLength); - } + Assert.IsFalse(publicKey1.SequenceEqual(publicKey2)); + Assert.IsFalse(privateKey1.SequenceEqual(privateKey2)); + } - [TestMethod] - public void TestGenerateKeysNoCache() - { - var keys1 = ECKeyHelper.GenerateKeys(); - var keys2 = ECKeyHelper.GenerateKeys(); + [TestMethod] + public void TestGetPrivateKey() + { + var privateKey = Base64UrlEncoder.DecodeBytes(TestPrivateKey); + var publicKey = Base64UrlEncoder.DecodeBytes(TestPublicKey); + var keypair = ECKeyHelper.GetKeyPair(privateKey, publicKey); - var publicKey1 = ((ECPublicKeyParameters) keys1.Public).Q.GetEncoded(false); - var privateKey1 = ((ECPrivateKeyParameters) keys1.Private).D.ToByteArrayUnsigned(); + var importedPrivateKey = keypair.GetEncodedPrivateKey(); + Assert.AreEqual(TestPrivateKey, importedPrivateKey); - var publicKey2 = ((ECPublicKeyParameters) keys2.Public).Q.GetEncoded(false); - var privateKey2 = ((ECPrivateKeyParameters) keys2.Private).D.ToByteArrayUnsigned(); + var rawPrivateKey = keypair.GetPrivateKey(); + Assert.IsTrue(privateKey.SequenceEqual(rawPrivateKey)); + } - Assert.IsFalse(publicKey1.SequenceEqual(publicKey2)); - Assert.IsFalse(privateKey1.SequenceEqual(privateKey2)); - } + [TestMethod] + public void TestGetPublicKey() + { + var privateKey = Base64UrlEncoder.DecodeBytes(TestPrivateKey); + var publicKey = Base64UrlEncoder.DecodeBytes(TestPublicKey); + var keypair = ECKeyHelper.GetKeyPair(privateKey, publicKey); - [TestMethod] - public void TestGetPrivateKey() - { - var privateKey = UrlBase64.Decode(TestPrivateKey); - var privateKeyParams = ECKeyHelper.GetPrivateKey(privateKey); + var importedPublicKey = keypair.GetEncodedPublicKey(); + Assert.AreEqual(TestPublicKey, importedPublicKey); - var importedPrivateKey = UrlBase64.Encode(privateKeyParams.D.ToByteArrayUnsigned()); + var rawPublicKey = keypair.GetPublicKey(); + Assert.IsTrue(publicKey.SequenceEqual(rawPublicKey)); + } - Assert.AreEqual(TestPrivateKey, importedPrivateKey); - } + [TestMethod] + public void TestGetKeyPairNet() + { + var privateKey = Base64UrlEncoder.DecodeBytes(TestPrivateKey); + var publicKey = Base64UrlEncoder.DecodeBytes(TestPublicKey); - [TestMethod] - public void TestGetPublicKey() - { - var publicKey = UrlBase64.Decode(TestPublicKey); - var publicKeyParams = ECKeyHelper.GetPublicKey(publicKey); + var keypair = ECKeyHelper.GetKeyPair(privateKey, publicKey); - var importedPublicKey = UrlBase64.Encode(publicKeyParams.Q.GetEncoded(false)); + var importedPublicKey = keypair.GetEncodedPublicKey(); + Assert.AreEqual(TestPublicKey, importedPublicKey); - Assert.AreEqual(TestPublicKey, importedPublicKey); - } + var importedPrivateKey = keypair.GetEncodedPrivateKey(); + Assert.AreEqual(TestPrivateKey, importedPrivateKey); } -} \ No newline at end of file +} diff --git a/WebPush.Test/JWSSignerTest.cs b/WebPush.Test/JWSSignerTest.cs index 184d755..1cae0ac 100644 --- a/WebPush.Test/JWSSignerTest.cs +++ b/WebPush.Test/JWSSignerTest.cs @@ -1,52 +1,63 @@ -using System.Collections.Generic; +using System; +using System.Security.Claims; using System.Text; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; using WebPush.Util; -namespace WebPush.Test -{ - [TestClass] - public class JWSSignerTest - { - private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; - - [TestMethod] - public void TestGenerateSignature() - { - var decodedPrivateKey = UrlBase64.Decode(TestPrivateKey); - var privateKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); - - var header = new Dictionary(); - header.Add("typ", "JWT"); - header.Add("alg", "ES256"); - - var jwtPayload = new Dictionary(); - jwtPayload.Add("aud", "aud"); - jwtPayload.Add("exp", 1); - jwtPayload.Add("sub", "subject"); - - var signer = new JwsSigner(privateKey); - var token = signer.GenerateSignature(header, jwtPayload); +namespace WebPush.Test; - var tokenParts = token.Split('.'); - - Assert.AreEqual(3, tokenParts.Length); - - var encodedHeader = tokenParts[0]; - var encodedPayload = tokenParts[1]; - var signature = tokenParts[2]; +[TestClass] +public class JWSSignerTest +{ + private const string TestPublicKey = + @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; - var decodedHeader = Encoding.UTF8.GetString(UrlBase64.Decode(encodedHeader)); - var decodedPayload = Encoding.UTF8.GetString(UrlBase64.Decode(encodedPayload)); + private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; - Assert.AreEqual(@"{""typ"":""JWT"",""alg"":""ES256""}", decodedHeader); - Assert.AreEqual(@"{""aud"":""aud"",""exp"":1,""sub"":""subject""}", decodedPayload); - var decodedSignature = UrlBase64.Decode(signature); - var decodedSignatureLength = decodedSignature.Length; + [TestMethod] + public void TestGenerateSignature() + { + var key = ECKeyHelper.GetKeyPair(Base64UrlEncoder.DecodeBytes(TestPrivateKey), Base64UrlEncoder.DecodeBytes(TestPublicKey)); + var handler = new JsonWebTokenHandler + { + SetDefaultTimesOnTokenCreation = false + }; + var now = DateTime.UtcNow; + var epoch = new DateTime(1970, 1, 1, 0, 0, 1, DateTimeKind.Utc); + var subject = new ClaimsIdentity([new Claim(JwtRegisteredClaimNames.Sub, "subject")]); - var isSignatureLengthValid = decodedSignatureLength == 66 || decodedSignatureLength == 64; - Assert.AreEqual(true, isSignatureLengthValid); - } + string token = handler.CreateToken(new SecurityTokenDescriptor + { + Audience = "aud", + // Expires = now.AddMinutes(1), + Expires = epoch, + // IssuedAt = now, + Subject = subject, + SigningCredentials = new SigningCredentials(new ECDsaSecurityKey(key), SecurityAlgorithms.EcdsaSha256), + }); + var tokenParts = token.Split('.'); + + Assert.HasCount(3, tokenParts); + + var encodedHeader = tokenParts[0]; + var encodedPayload = tokenParts[1]; + var signature = tokenParts[2]; + + var decodedHeader = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(encodedHeader)); + var decodedPayload = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(encodedPayload)); + + Assert.AreEqual(@"{""alg"":""ES256"",""typ"":""JWT""}", decodedHeader); + Assert.Contains(@"""typ"":""JWT""", decodedHeader); + Assert.Contains(@"""alg"":""ES256""", decodedHeader); + Assert.AreEqual(@"{""aud"":""aud"",""exp"":1,""sub"":""subject""}", decodedPayload); + + var decodedSignature = Base64UrlEncoder.DecodeBytes(signature); + var decodedSignatureLength = decodedSignature.Length; + + var isSignatureLengthValid = decodedSignatureLength == 66 || decodedSignatureLength == 64; + Assert.IsTrue(isSignatureLengthValid); } -} \ No newline at end of file +} diff --git a/WebPush.Test/UrlBase64Test.cs b/WebPush.Test/UrlBase64Test.cs index 5636517..e351daa 100644 --- a/WebPush.Test/UrlBase64Test.cs +++ b/WebPush.Test/UrlBase64Test.cs @@ -1,26 +1,27 @@ using System.Linq; +using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; -using WebPush.Util; -namespace WebPush.Test +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] + +namespace WebPush.Test; + +[TestClass] +public class UrlBase64Test { - [TestClass] - public class UrlBase64Test + [TestMethod] + public void TestBase64UrlDecode() { - [TestMethod] - public void TestBase64UrlDecode() - { - var expected = new byte[3] {181, 235, 45}; - var actual = UrlBase64.Decode(@"test"); - Assert.IsTrue(actual.SequenceEqual(expected)); - } + var expected = new byte[3] { 181, 235, 45 }; + var actual = Base64UrlEncoder.DecodeBytes(@"test"); + Assert.IsTrue(actual.SequenceEqual(expected)); + } - [TestMethod] - public void TestBase64UrlEncode() - { - var expected = @"test"; - var actual = UrlBase64.Encode(new byte[3] {181, 235, 45}); - Assert.AreEqual(expected, actual); - } + [TestMethod] + public void TestBase64UrlEncode() + { + var expected = @"test"; + var actual = Base64UrlEncoder.Encode([181, 235, 45]); + Assert.AreEqual(expected, actual); } -} \ No newline at end of file +} diff --git a/WebPush.Test/VapidHelperTest.cs b/WebPush.Test/VapidHelperTest.cs index 73ba0e6..028d5e5 100644 --- a/WebPush.Test/VapidHelperTest.cs +++ b/WebPush.Test/VapidHelperTest.cs @@ -1,119 +1,217 @@ using System; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; -using WebPush.Util; +using WebPush.Model; -namespace WebPush.Test +namespace WebPush.Test; + +[TestClass] +public class VapidHelperTest { - [TestClass] - public class VapidHelperTest - { - private const string ValidAudience = @"http://example.com"; - private const string ValidSubject = @"http://example.com/example"; - private const string ValidSubjectMailto = @"mailto:example@example.com"; - - private const string TestPublicKey = - @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; - - private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; - - [TestMethod] - public void TestGenerateVapidKeys() - { - var keys = VapidHelper.GenerateVapidKeys(); - var publicKey = UrlBase64.Decode(keys.PublicKey); - var privateKey = UrlBase64.Decode(keys.PrivateKey); - - Assert.AreEqual(32, privateKey.Length); - Assert.AreEqual(65, publicKey.Length); - } - - [TestMethod] - public void TestGenerateVapidKeysNoCache() - { - var keys1 = VapidHelper.GenerateVapidKeys(); - var keys2 = VapidHelper.GenerateVapidKeys(); - - Assert.AreNotEqual(keys1.PublicKey, keys2.PublicKey); - Assert.AreNotEqual(keys1.PrivateKey, keys2.PrivateKey); - } - - [TestMethod] - public void TestGetVapidHeaders() - { - var publicKey = TestPublicKey; - var privateKey = TestPrivateKey; - var headers = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); - - Assert.IsTrue(headers.ContainsKey(@"Authorization")); - Assert.IsTrue(headers.ContainsKey(@"Crypto-Key")); - } - - [TestMethod] - public void TestGetVapidHeadersAudienceNotAUrl() - { - var publicKey = TestPublicKey; - var privateKey = TestPrivateKey; - Assert.Throws( - delegate - { - VapidHelper.GetVapidHeaders("invalid audience", ValidSubjectMailto, publicKey, privateKey); - }); - } - - [TestMethod] - public void TestGetVapidHeadersInvalidPrivateKey() - { - var publicKey = UrlBase64.Encode(new byte[65]); - var privateKey = UrlBase64.Encode(new byte[1]); - - Assert.Throws( - delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); }); - } - - [TestMethod] - public void TestGetVapidHeadersInvalidPublicKey() - { - var publicKey = UrlBase64.Encode(new byte[1]); - var privateKey = UrlBase64.Encode(new byte[32]); - - Assert.Throws( - delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); }); - } - - [TestMethod] - public void TestGetVapidHeadersSubjectNotAUrlOrMailTo() - { - var publicKey = TestPublicKey; - var privateKey = TestPrivateKey; - - Assert.Throws( - delegate { VapidHelper.GetVapidHeaders(ValidAudience, @"invalid subject", publicKey, privateKey); }); - } - - [TestMethod] - public void TestGetVapidHeadersWithMailToSubject() - { - var publicKey = TestPublicKey; - var privateKey = TestPrivateKey; - var headers = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, publicKey, - privateKey); - - Assert.IsTrue(headers.ContainsKey(@"Authorization")); - Assert.IsTrue(headers.ContainsKey(@"Crypto-Key")); - } - - [TestMethod] - public void TestExpirationInPastExceptions() - { - var publicKey = TestPublicKey; - var privateKey = TestPrivateKey; - - Assert.Throws( - delegate - { - VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, publicKey, - privateKey, 1552715607); - }); - } - } -} \ No newline at end of file + private const string ValidAudience = @"http://example.com"; + private const string ValidSubject = @"http://example.com/example"; + private const string ValidSubjectMailto = @"mailto:example@example.com"; + + private const string TestPublicKey = + @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; + + private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; + + [TestMethod] + public void TestGenerateVapidKeys() + { + var keys = VapidHelper.GenerateVapidKeys(); + var publicKey = Base64UrlEncoder.DecodeBytes(keys.PublicKey); + var privateKey = Base64UrlEncoder.DecodeBytes(keys.PrivateKey); + + Assert.HasCount(32, privateKey); + Assert.HasCount(65, publicKey); + } + + [TestMethod] + public void TestGenerateVapidKeysNoCache() + { + var keys1 = VapidHelper.GenerateVapidKeys(); + var keys2 = VapidHelper.GenerateVapidKeys(); + + Assert.AreNotEqual(keys1.PublicKey, keys2.PublicKey); + Assert.AreNotEqual(keys1.PrivateKey, keys2.PrivateKey); + } + + [TestMethod] + public void TestGetVapidHeaders() + { + var publicKey = TestPublicKey; + var privateKey = TestPrivateKey; + var headers = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); + + Assert.IsTrue(headers.ContainsKey(@"Authorization")); + } + + [TestMethod] + public void TestGetVapidHeadersAudienceNotAUrl() + { + var publicKey = TestPublicKey; + var privateKey = TestPrivateKey; + Assert.ThrowsExactly( + delegate + { + VapidHelper.GetVapidHeaders("invalid audience", ValidSubjectMailto, publicKey, privateKey); + }); + } + + [TestMethod] + public void TestGetVapidHeadersAudienceMissing() + { + var publicKey = TestPublicKey; + var privateKey = TestPrivateKey; + Assert.ThrowsExactly( + delegate + { + VapidHelper.GetVapidHeaders("", ValidSubjectMailto, publicKey, privateKey); + }); + } + + + [TestMethod] + public void TestGetVapidHeadersInvalidPrivateKey() + { + var publicKey = Base64UrlEncoder.Encode(new byte[65]); + var privateKey = Base64UrlEncoder.Encode(new byte[1]); + + Assert.ThrowsExactly( + delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); }); + } + + [TestMethod] + public void TestGetVapidHeadersInvalidPublicKey() + { + var publicKey = Base64UrlEncoder.Encode(new byte[1]); + var privateKey = Base64UrlEncoder.Encode(new byte[32]); + + Assert.ThrowsExactly( + delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); }); + } + + [TestMethod] + public void TestGetVapidHeadersPublicKeyMissing() + { + Assert.ThrowsExactly( + delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, "", TestPrivateKey); }); + } + + [TestMethod] + public void TestGetVapidHeadersPublicKeyInvalidBase64() + { + Assert.Throws( + delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33zJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A", TestPrivateKey); }); + } + + [TestMethod] + public void TestGetVapidHeadersPrivateKeyMissing() + { + Assert.ThrowsExactly( + delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, TestPublicKey, ""); }); + } + + [TestMethod] + public void TestGetVapidHeadersPrivateKeyInvalidBase64() + { + Assert.Throws( + delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, TestPublicKey, @"WRONGKmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"); }); + } + + [TestMethod] + public void TestGetVapidHeadersSubjectNotAUrlOrMailTo() + { + var publicKey = TestPublicKey; + var privateKey = TestPrivateKey; + + Assert.ThrowsExactly( + delegate { VapidHelper.GetVapidHeaders(ValidAudience, @"invalid subject", publicKey, privateKey); }); + } + + [TestMethod] + public void TestGetVapidHeadersSubjectMissing() + { + var publicKey = TestPublicKey; + var privateKey = TestPrivateKey; + + Assert.ThrowsExactly( + delegate { VapidHelper.GetVapidHeaders(ValidAudience, " ", publicKey, privateKey); }); + } + + [TestMethod] + public void TestGetVapidHeadersWithMailToSubject() + { + var publicKey = TestPublicKey; + var privateKey = TestPrivateKey; + var headers = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, publicKey, + privateKey); + + Assert.IsTrue(headers.ContainsKey(@"Authorization")); + } + + [TestMethod] + public void TestExpirationInPastExceptions() + { + var publicKey = TestPublicKey; + var privateKey = TestPrivateKey; + + Assert.ThrowsExactly( + delegate + { + VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, publicKey, + privateKey, DateTimeOffset.FromUnixTimeSeconds(1552715607).UtcDateTime); + }); + } + + + [TestMethod] + public void TestVapidHeaders() + { + var vapidHeaders = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, TestPublicKey, TestPrivateKey, new DateTime(2128, 08, 08, 08, 08, 08), ContentEncoding.Aes128gcm); + + vapidHeaders.TryGetValue("Authorization", out var authHeader); + Assert.IsNotNull(vapidHeaders); + + var partsSpace = authHeader.Split(' '); + Assert.IsGreaterThanOrEqualTo(3, partsSpace.Length); + + var authType = partsSpace[0]; + Assert.AreEqual("vapid", authType); + + var jwkPart = partsSpace[1][0..^1]; // remove delimiter ',' + Assert.StartsWith("t=", jwkPart); + var token = jwkPart["t=".Length..]; + var tokenParts = token.Split('.'); + + Assert.HasCount(3, tokenParts); + + var encodedHeader = tokenParts[0]; + var encodedPayload = tokenParts[1]; + var signature = tokenParts[2]; + + var decodedHeader = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(encodedHeader)); + var decodedPayload = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(encodedPayload)); + + Assert.AreEqual(@"{""alg"":""ES256"",""typ"":""JWT""}", decodedHeader); + Assert.Contains(@"""typ"":""JWT""", decodedHeader); + Assert.Contains(@"""alg"":""ES256""", decodedHeader); + Assert.Contains(@$"""aud"":""{ValidAudience}""", decodedPayload); + Assert.Contains(@$"""sub"":""{ValidSubjectMailto}""", decodedPayload); + Assert.MatchesRegex(@"""exp"":\d+", decodedPayload); + + var decodedSignature = Base64UrlEncoder.DecodeBytes(signature); + var decodedSignatureLength = decodedSignature.Length; + + var isSignatureLengthValid = decodedSignatureLength == 66 || decodedSignatureLength == 64; + Assert.IsTrue(isSignatureLengthValid); + + var keyPart = partsSpace[2]; + Assert.StartsWith("k=", keyPart); + Assert.AreEqual(TestPublicKey, keyPart["k-".Length..]); + } +} diff --git a/WebPush.Test/WebPush.Test.csproj b/WebPush.Test/WebPush.Test.csproj index b31f159..2fc9cd4 100755 --- a/WebPush.Test/WebPush.Test.csproj +++ b/WebPush.Test/WebPush.Test.csproj @@ -1,15 +1,16 @@  - net48;net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0 false + true - + - - + + diff --git a/WebPush.Test/WebPushClientTest.cs b/WebPush.Test/WebPushClientTest.cs index 5032d79..e556758 100644 --- a/WebPush.Test/WebPushClientTest.cs +++ b/WebPush.Test/WebPushClientTest.cs @@ -1,185 +1,143 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using RichardSzalay.MockHttp; -using System; -using System.Collections.Generic; +using System; using System.Linq; using System.Net; using System.Net.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RichardSzalay.MockHttp; using WebPush.Model; -namespace WebPush.Test +namespace WebPush.Test; + +[TestClass] +public class WebPushClientTest { - [TestClass] - public class WebPushClientTest + private const string TestPublicKey = + @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; + + private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; + + private const string TestGcmEndpoint = @"https://android.googleapis.com/gcm/send/"; + + private const string TestFcmEndpoint = + @"https://fcm.googleapis.com/fcm/send/efz_TLX_rLU:APA91bE6U0iybLYvv0F3mf6"; + + private const string TestFirefoxEndpoint = + @"https://updates.push.services.mozilla.com/wpush/v2/gBABAABgOe_sGrdrsT35ljtA4O9xCX"; + + public const string TestSubject = "mailto:example@example.com"; + + private MockHttpMessageHandler httpMessageHandlerMock; + private WebPushClient client; + + [TestInitialize] + public void InitializeTest() { - private const string TestPublicKey = - @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; - - private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; - - private const string TestGcmEndpoint = @"https://android.googleapis.com/gcm/send/"; - - private const string TestFcmEndpoint = - @"https://fcm.googleapis.com/fcm/send/efz_TLX_rLU:APA91bE6U0iybLYvv0F3mf6"; - - private const string TestFirefoxEndpoint = - @"https://updates.push.services.mozilla.com/wpush/v2/gBABAABgOe_sGrdrsT35ljtA4O9xCX"; - - public const string TestSubject = "mailto:example@example.com"; - - private MockHttpMessageHandler httpMessageHandlerMock; - private WebPushClient client; - - [TestInitialize] - public void InitializeTest() - { - httpMessageHandlerMock = new MockHttpMessageHandler(); - client = new WebPushClient(httpMessageHandlerMock.ToHttpClient()); - } - - [TestMethod] - public void TestGcmApiKeyInOptions() - { - var gcmAPIKey = @"teststring"; - var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); - - var options = new Dictionary(); - options[@"gcmAPIKey"] = gcmAPIKey; - var message = client.GenerateRequestDetails(subscription, @"test payload", options); - var authorizationHeader = message.Headers.GetValues(@"Authorization").First(); - - Assert.AreEqual("key=" + gcmAPIKey, authorizationHeader); - - // Test previous incorrect casing of gcmAPIKey - var options2 = new Dictionary(); - options2[@"gcmApiKey"] = gcmAPIKey; - Assert.Throws(delegate - { - client.GenerateRequestDetails(subscription, "test payload", options2); - }); - } - - [TestMethod] - public void TestSetGcmApiKey() - { - var gcmAPIKey = @"teststring"; - client.SetGcmApiKey(gcmAPIKey); - var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); - var message = client.GenerateRequestDetails(subscription, @"test payload"); - var authorizationHeader = message.Headers.GetValues(@"Authorization").First(); - - Assert.AreEqual(@"key=" + gcmAPIKey, authorizationHeader); - } - - [TestMethod] - public void TestSetGCMAPIKeyEmptyString() - { - Assert.Throws(delegate - { client.SetGcmApiKey(""); }); - } - - [TestMethod] - public void TestSetGcmApiKeyNonGcmPushService() - { - // Ensure that the API key doesn't get added on a service that doesn't accept it. - var gcmAPIKey = @"teststring"; - client.SetGcmApiKey(gcmAPIKey); - var subscription = new PushSubscription(TestFirefoxEndpoint, TestPublicKey, TestPrivateKey); - var message = client.GenerateRequestDetails(subscription, @"test payload"); - - Assert.IsFalse(message.Headers.TryGetValues(@"Authorization", out var values)); - } - - [TestMethod] - public void TestSetGcmApiKeyNull() - { - client.SetGcmApiKey(@"somestring"); - client.SetGcmApiKey(null); - - var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); - var message = client.GenerateRequestDetails(subscription, @"test payload"); - - Assert.IsFalse(message.Headers.TryGetValues("Authorization", out var values)); - } - - [TestMethod] - public void TestSetVapidDetails() - { - client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey); - - var subscription = new PushSubscription(TestFirefoxEndpoint, TestPublicKey, TestPrivateKey); - var message = client.GenerateRequestDetails(subscription, @"test payload"); - var authorizationHeader = message.Headers.GetValues(@"Authorization").First(); - var cryptoHeader = message.Headers.GetValues(@"Crypto-Key").First(); - - Assert.IsTrue(authorizationHeader.StartsWith(@"WebPush ")); - Assert.IsTrue(cryptoHeader.Contains(@"p256ecdsa")); - } - - [TestMethod] - public void TestFcmAddsAuthorizationHeader() - { - client.SetGcmApiKey(@"somestring"); - var subscription = new PushSubscription(TestFcmEndpoint, TestPublicKey, TestPrivateKey); - var message = client.GenerateRequestDetails(subscription, @"test payload"); - var authorizationHeader = message.Headers.GetValues(@"Authorization").First(); - - Assert.IsTrue(authorizationHeader.StartsWith(@"key=")); - } - - [TestMethod] - [DataRow(HttpStatusCode.Created)] - [DataRow(HttpStatusCode.Accepted)] - public void TestHandlingSuccessHttpCodes(HttpStatusCode status) - { - TestSendNotification(status); - } - - [TestMethod] - [DataRow(HttpStatusCode.BadRequest, "Bad Request")] - [DataRow(HttpStatusCode.RequestEntityTooLarge, "Payload too large")] - [DataRow((HttpStatusCode)429, "Too many request")] - [DataRow(HttpStatusCode.NotFound, "Subscription no longer valid")] - [DataRow(HttpStatusCode.Gone, "Subscription no longer valid")] - [DataRow(HttpStatusCode.InternalServerError, "Received unexpected response code: 500")] - public void TestHandlingFailureHttpCodes(HttpStatusCode status, string expectedMessage) - { - var actual = Assert.Throws(() => TestSendNotification(status)); - Assert.AreEqual(expectedMessage, actual.Message); - } - - [TestMethod] - [DataRow(HttpStatusCode.BadRequest, "authorization key missing", "Bad Request. Details: authorization key missing")] - [DataRow(HttpStatusCode.RequestEntityTooLarge, "max size is 512", "Payload too large. Details: max size is 512")] - [DataRow((HttpStatusCode)429, "the api is limited", "Too many request. Details: the api is limited")] - [DataRow(HttpStatusCode.NotFound, "", "Subscription no longer valid")] - [DataRow(HttpStatusCode.Gone, "", "Subscription no longer valid")] - [DataRow(HttpStatusCode.InternalServerError, "internal error", "Received unexpected response code: 500. Details: internal error")] - public void TestHandlingFailureMessages(HttpStatusCode status, string response, string expectedMessage) - { - var actual = Assert.Throws(() => TestSendNotification(status, response)); - Assert.AreEqual(expectedMessage, actual.Message); - } - - [TestMethod] - [DataRow(1)] - [DataRow(5)] - [DataRow(10)] - [DataRow(50)] - public void TestHandleInvalidPublicKeys(int charactersToDrop) - { - var invalidKey = TestPublicKey.Substring(0, TestPublicKey.Length - charactersToDrop); - - Assert.Throws(() => TestSendNotification(HttpStatusCode.OK, response: null, invalidKey)); - } - - private void TestSendNotification(HttpStatusCode status, string response = null, string publicKey = TestPublicKey) - { - var subscription = new PushSubscription(TestFcmEndpoint, publicKey, TestPrivateKey); - var httpContent = response == null ? null : new StringContent(response); - httpMessageHandlerMock.When(TestFcmEndpoint).Respond(req => new HttpResponseMessage { StatusCode = status, Content = httpContent }); - client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey); - client.SendNotification(subscription, "123"); - } + httpMessageHandlerMock = new MockHttpMessageHandler(); + client = new WebPushClient(httpMessageHandlerMock.ToHttpClient()); } -} \ No newline at end of file + + [TestMethod] + public void TestSetTopic() + { + var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); + var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { Topic = "testtopic" }); + Assert.AreEqual(@"testtopic", message.Headers.GetValues(@"Topic").First()); + } + + [TestMethod] + public void TestSetTopicFailures() + { + var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); + Assert.ThrowsExactly(() => client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { Topic = "failing topic #3" })); + } + + + [TestMethod] + public void TestSetUrgency() + { + var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); + var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { Urgency = Urgency.VeryLow }); + Assert.AreEqual(@"very-low", message.Headers.GetValues(@"Urgency").First()); + } + + [TestMethod] + public void TestSetContentEncoding() + { + var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); + var messageAes128gcm = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { ContentEncoding = ContentEncoding.Aes128gcm }); + Assert.AreEqual(@"aes128gcm", messageAes128gcm.Content.Headers.ContentEncoding.First()); + var messageAesgcm = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { ContentEncoding = ContentEncoding.Aesgcm }); + Assert.AreEqual(@"aesgcm", messageAesgcm.Content.Headers.ContentEncoding.First()); + } + + + [TestMethod] + public void TestSetVapidDetails() + { + client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey); + + var subscription = new PushSubscription(TestFirefoxEndpoint, TestPublicKey, TestPrivateKey); + var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions()); + var authorizationHeader = message.Headers.GetValues(@"Authorization").First(); + // var cryptoHeader = message.Headers.GetValues(@"Crypto-Key").First(); + + // Assert.StartsWith(@"WebPush ", authorizationHeader); + Assert.StartsWith(@"vapid ", authorizationHeader); + // Assert.Contains(@"p256ecdsa", cryptoHeader); + } + + [TestMethod] + [DataRow(HttpStatusCode.Created)] + [DataRow(HttpStatusCode.Accepted)] + public void TestHandlingSuccessHttpCodes(HttpStatusCode status) + { + TestSendNotification(status); + } + + [TestMethod] + [DataRow(HttpStatusCode.BadRequest, "Bad Request")] + [DataRow(HttpStatusCode.RequestEntityTooLarge, "Payload too large")] + [DataRow((HttpStatusCode)429, "Too many request")] + [DataRow(HttpStatusCode.NotFound, "Subscription no longer valid")] + [DataRow(HttpStatusCode.Gone, "Subscription no longer valid")] + [DataRow(HttpStatusCode.InternalServerError, "Received unexpected response code: 500")] + public void TestHandlingFailureHttpCodes(HttpStatusCode status, string expectedMessage) + { + var actual = Assert.ThrowsExactly(() => TestSendNotification(status)); + Assert.AreEqual(expectedMessage, actual.Message); + } + + [TestMethod] + [DataRow(HttpStatusCode.BadRequest, "authorization key missing", "Bad Request. Details: authorization key missing")] + [DataRow(HttpStatusCode.RequestEntityTooLarge, "max size is 512", "Payload too large. Details: max size is 512")] + [DataRow((HttpStatusCode)429, "the api is limited", "Too many request. Details: the api is limited")] + [DataRow(HttpStatusCode.NotFound, "", "Subscription no longer valid")] + [DataRow(HttpStatusCode.Gone, "", "Subscription no longer valid")] + [DataRow(HttpStatusCode.InternalServerError, "internal error", "Received unexpected response code: 500. Details: internal error")] + public void TestHandlingFailureMessages(HttpStatusCode status, string response, string expectedMessage) + { + var actual = Assert.ThrowsExactly(() => TestSendNotification(status, response)); + Assert.AreEqual(expectedMessage, actual.Message); + } + + [TestMethod] + [DataRow(1)] + [DataRow(5)] + [DataRow(10)] + [DataRow(50)] + public void TestHandleInvalidPublicKeys(int charactersToDrop) + { + var invalidKey = TestPublicKey.Substring(0, TestPublicKey.Length - charactersToDrop); + Assert.ThrowsExactly(() => TestSendNotification(HttpStatusCode.OK, response: null, invalidKey)); + } + + private void TestSendNotification(HttpStatusCode status, string response = null, string publicKey = TestPublicKey) + { + var subscription = new PushSubscription(TestFcmEndpoint, publicKey, TestPrivateKey); + var httpContent = response == null ? null : new StringContent(response); + httpMessageHandlerMock.When(TestFcmEndpoint).Respond(req => new HttpResponseMessage { StatusCode = status, Content = httpContent }); + client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey); + client.SendNotification(subscription, "123", new WebPushOptions()); + } + +} diff --git a/WebPush.Test/packages.lock.json b/WebPush.Test/packages.lock.json index e00192a..02cdb60 100644 --- a/WebPush.Test/packages.lock.json +++ b/WebPush.Test/packages.lock.json @@ -1,23 +1,15 @@ { "version": 1, "dependencies": { - ".NETFramework,Version=v4.8": { + "net10.0": { "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[18.4.0, )", - "resolved": "18.4.0", - "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==", - "dependencies": { - "Microsoft.CodeCoverage": "18.4.0" - } - }, - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "requested": "[18.5.1, )", + "resolved": "18.5.1", + "contentHash": "SfqVaLiIqAbRWuPg5BP4QFwBIirQj/YIL8Dhxl6zntBKbXp0cQykoV480SmwG+yRMiWptxEI6NbHQuGSZ8b97w==", "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" + "Microsoft.CodeCoverage": "18.5.1", + "Microsoft.TestPlatform.TestHost": "18.5.1" } }, "Moq": { @@ -26,29 +18,27 @@ "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { - "Castle.Core": "5.1.1", - "System.Threading.Tasks.Extensions": "4.5.4" + "Castle.Core": "5.1.1" } }, "MSTest.TestAdapter": { "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "lZRgNzaQnffK4XLjM/og4Eoqp/3IkpcyJQQcyKXkPdkzCT3+ghpwHa9zG1xYhQDbUFoc54M+/waLwh31K9stDQ==", + "requested": "[4.2.2, )", + "resolved": "4.2.2", + "contentHash": "gMKNPoBnnlYM1DY+zAxJP05LDgXNHkjqxj6QQsm/O71nZh5BJ2SzsaTaQBQhXlu/HjzQ2CCbnMgufU13kYIpVA==", "dependencies": { - "MSTest.TestFramework": "4.2.1", - "Microsoft.Testing.Extensions.VSTestBridge": "2.2.1", - "Microsoft.Testing.Platform.MSBuild": "2.2.1", - "System.Threading.Tasks.Extensions": "4.5.4" + "MSTest.TestFramework": "4.2.2", + "Microsoft.Testing.Extensions.VSTestBridge": "2.2.2", + "Microsoft.Testing.Platform.MSBuild": "2.2.2" } }, "MSTest.TestFramework": { "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "I4/RbS2TpGZ56CE98+jPbrGlcerYtw2LvPVKzQGvyQQcJDekPy2Kd+fnThXYn+geJ1sW+vA9B7++rFNxvKcWxA==", + "requested": "[4.2.2, )", + "resolved": "4.2.2", + "contentHash": "IGjOt2kE6NxIgWYcM40DYSzCFaajLe6wHEICPRBnCqj1K4f9HrBLMPo4PE4mM/uKHNgDBvhvj/t1bXenUcQKqQ==", "dependencies": { - "MSTest.Analyzers": "4.2.1" + "MSTest.Analyzers": "4.2.2" } }, "RichardSzalay.MockHttp": { @@ -60,336 +50,129 @@ "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { - "System.Diagnostics.DiagnosticSource": "5.0.0" + "System.Diagnostics.EventLog": "6.0.0" } }, - "Microsoft.Bcl.AsyncInterfaces": { + "Microsoft.ApplicationInsights": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "g0Xp9A+B8jCf5pNIIhFOQXPJkte3D87shfTLY+ylwfSh22U5oQH6tvvmcUuqJvt/wtwKk0WdNp2OGEczHJlJdg==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.6.3" - } + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" + "resolved": "18.5.1", + "contentHash": "vMFDR1ZjqzzgKmM0zrPie7Gv9Y+ZppjODB5Quzu9Eq0TlIusUfUCYFPEawO91zQuqwzvdFbJSU7WHNtjStffJQ==" }, - "Microsoft.NETFramework.ReferenceAssemblies.net48": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==" - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "7zB8BjffOyvqfHF26rFVPuK0w1fCf5+j1tLuhHIr76CqxXkGb+fMJtq6YNOV+m6qPytExHMXxluk3RgJ+dSIqw==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.2.1", - "System.Diagnostics.DiagnosticSource": "6.0.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "RD6D1Jx6cKDA5IHd1H2q8ylIuQG3PD+gdULI0JC8CvsRtaypFzTFpB5xDPuQi8o6kAkcM04cBhAiJPxZboNH2Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.2.1" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "D8AGlkNtlTQPe3zf4SLnHBMr13lerMe0RuHSoRfnRatcuX/T7YbRtgn39rWBjKhXsNio0WXKrPKv3gfWE2I46w==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.3.0", - "Microsoft.Testing.Extensions.Telemetry": "2.2.1", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.1", - "Microsoft.Testing.Platform": "2.2.1" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "9bbPuls/b6/vUFzxbSjJLZlJHyKBfOZE5kjIY+ITI2ASqlFPJhR83BdLydJeQOCLEZhEbrEcz5xtt1B69nwSVg==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "CSJOcZHfKlTyPbS0CTJk6iEnU4gJC+eUA5z72UBnMDRdgVHYOmB8k9Y7jT233gZjnCOQiYFg3acQHRfu2H62nw==", - "dependencies": { - "Microsoft.Testing.Platform": "2.2.1" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.3.0", - "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==", - "dependencies": { - "System.Reflection.Metadata": "8.0.0" - } - }, - "MSTest.Analyzers": { - "type": "Transitive", - "resolved": "4.2.1", - "contentHash": "1i9jgE/42KGGyZ4s0MdrYM/Uu/dRYhbRfYQifcO0AZ6vw4sBXRjoQGQRGNSm771AYgPAmoGl0u4sJc2lMET6HQ==" - }, - "Portable.BouncyCastle": { - "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" - }, - "System.Collections.Immutable": { + "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", - "dependencies": { - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==", - "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "LTxXYYKmRhPKWveYmfzuRTUnzsfY7CN+WOq6aTRgYE9vJ8BUvIWPCaSx4HxqBwXViTPSjR9cHDOVuVPuZGRR/Q==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Threading.Tasks.Extensions": "4.6.3" - } + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Numerics.Vectors": "4.6.1", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" - }, - "System.Reflection.Metadata": { + "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "8.0.0", - "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", - "dependencies": { - "System.Collections.Immutable": "8.0.0", - "System.Memory": "4.5.5" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.1.2", - "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "WUH+viO8VDG8NpFKvOBwpeyKUiPOMz3kQpA6AKCD4b2NG1pBhyC4AwTb357iZmTxZDnkM4IsFnvzN8W8OKmsHg==", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, - "System.Text.Json": { + "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "F8Pu2QLUMeniVbtiyk7n7LCfFYxlcJ8ASaSwglJyq6dxa34iCQrikQszsgJClIJWuSWjcyhKkV7daAzYJqeVwA==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.7", - "System.Buffers": "4.6.1", - "System.IO.Pipelines": "10.0.7", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2", - "System.Text.Encodings.Web": "10.0.7", - "System.Threading.Tasks.Extensions": "4.6.3", - "System.ValueTuple": "4.6.2" - } + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, - "System.Threading.Tasks.Extensions": { + "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, - "System.ValueTuple": { + "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "4.6.2", - "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" - }, - "webpush": { - "type": "Project", - "dependencies": { - "Portable.BouncyCastle": "[1.9.0, )", - "System.Text.Json": "[10.0.7, )" - } - } - }, - "net10.0": { - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[18.4.0, )", - "resolved": "18.4.0", - "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==", - "dependencies": { - "Microsoft.CodeCoverage": "18.4.0", - "Microsoft.TestPlatform.TestHost": "18.4.0" - } - }, - "Moq": { - "type": "Direct", - "requested": "[4.20.72, )", - "resolved": "4.20.72", - "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "Castle.Core": "5.1.1" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, - "MSTest.TestAdapter": { - "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "lZRgNzaQnffK4XLjM/og4Eoqp/3IkpcyJQQcyKXkPdkzCT3+ghpwHa9zG1xYhQDbUFoc54M+/waLwh31K9stDQ==", - "dependencies": { - "MSTest.TestFramework": "4.2.1", - "Microsoft.Testing.Extensions.VSTestBridge": "2.2.1", - "Microsoft.Testing.Platform.MSBuild": "2.2.1" - } - }, - "MSTest.TestFramework": { - "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "I4/RbS2TpGZ56CE98+jPbrGlcerYtw2LvPVKzQGvyQQcJDekPy2Kd+fnThXYn+geJ1sW+vA9B7++rFNxvKcWxA==", - "dependencies": { - "MSTest.Analyzers": "4.2.1" - } - }, - "RichardSzalay.MockHttp": { - "type": "Direct", - "requested": "[7.0.0, )", - "resolved": "7.0.0", - "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg==" - }, - "Castle.Core": { + "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { - "System.Diagnostics.EventLog": "6.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.18.0" } }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" - }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "7zB8BjffOyvqfHF26rFVPuK0w1fCf5+j1tLuhHIr76CqxXkGb+fMJtq6YNOV+m6qPytExHMXxluk3RgJ+dSIqw==", + "resolved": "2.2.2", + "contentHash": "qKRghdaDiC88N1s3LDJO7zW74QNZu/ErnTxuG7R9u9UORn6pTwdqbi7X+eY4UQb+7YV2gR2yz8eRelvOWQVxhA==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "RD6D1Jx6cKDA5IHd1H2q8ylIuQG3PD+gdULI0JC8CvsRtaypFzTFpB5xDPuQi8o6kAkcM04cBhAiJPxZboNH2Q==", + "resolved": "2.2.2", + "contentHash": "MuOC3Be70FPysaPxaO0f3GFoXU49UwnKCVDWfFrOZ93h955KZ6MKiJ6vwt/2r4e1wkLDoJFbkQzi/MNbpe4oXQ==", "dependencies": { - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Extensions.VSTestBridge": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "D8AGlkNtlTQPe3zf4SLnHBMr13lerMe0RuHSoRfnRatcuX/T7YbRtgn39rWBjKhXsNio0WXKrPKv3gfWE2I46w==", + "resolved": "2.2.2", + "contentHash": "dyo49lXzY3seyfEgv7qrkIqdvrMAjdJjmY0VDPE//UPK89c+65cqQm8m+FO5XbRpr8gB6AUi5KCRbEl1eRlwQA==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", - "Microsoft.Testing.Extensions.Telemetry": "2.2.1", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.1", - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Extensions.Telemetry": "2.2.2", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.2", + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Platform": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "9bbPuls/b6/vUFzxbSjJLZlJHyKBfOZE5kjIY+ITI2ASqlFPJhR83BdLydJeQOCLEZhEbrEcz5xtt1B69nwSVg==" + "resolved": "2.2.2", + "contentHash": "9mUsTOri0aVqBX7/EJwqVJxVwdOzGUVJqK1H2EMfIl9xxJuSdqhfAlJbukl/iNugvi4+cmQs/LI8PLTDUT9P1A==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "CSJOcZHfKlTyPbS0CTJk6iEnU4gJC+eUA5z72UBnMDRdgVHYOmB8k9Y7jT233gZjnCOQiYFg3acQHRfu2H62nw==", + "resolved": "2.2.2", + "contentHash": "acgkTLYA8C39oe5b5ISmydBshR0XO6v8z3/CXAsLmPQ3xAiomHuPoTAgY28tjQLcwPZOu4GX034BXWvmsVpzIg==", "dependencies": { - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg==" + "resolved": "18.5.1", + "contentHash": "KNZd+M0S0rz5eNAln0pbZX+A/RbokYZCbGKx4fN4CkhtWhkz6nSJDO+9LGYjRE4d0WPVriJ2JnVubkjt3+PpMg==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==", + "resolved": "18.5.1", + "contentHash": "RM+3JNHEoHOCFXzVntUcIiYxzPjzBN0N8wto6HYXi76YyBTZ/3CeRL8U+Pk5zx3AUrOmHxDvKJwGUCdElU9bJg==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.4.0", + "Microsoft.TestPlatform.ObjectModel": "18.5.1", "Newtonsoft.Json": "13.0.3" } }, "MSTest.Analyzers": { "type": "Transitive", - "resolved": "4.2.1", - "contentHash": "1i9jgE/42KGGyZ4s0MdrYM/Uu/dRYhbRfYQifcO0AZ6vw4sBXRjoQGQRGNSm771AYgPAmoGl0u4sJc2lMET6HQ==" + "resolved": "4.2.2", + "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "Portable.BouncyCastle": { - "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", @@ -398,19 +181,20 @@ "webpush": { "type": "Project", "dependencies": { - "Portable.BouncyCastle": "[1.9.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )", + "Microsoft.IdentityModel.Tokens": "[8.18.0, )" } } }, "net8.0": { "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[18.4.0, )", - "resolved": "18.4.0", - "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==", + "requested": "[18.5.1, )", + "resolved": "18.5.1", + "contentHash": "SfqVaLiIqAbRWuPg5BP4QFwBIirQj/YIL8Dhxl6zntBKbXp0cQykoV480SmwG+yRMiWptxEI6NbHQuGSZ8b97w==", "dependencies": { - "Microsoft.CodeCoverage": "18.4.0", - "Microsoft.TestPlatform.TestHost": "18.4.0" + "Microsoft.CodeCoverage": "18.5.1", + "Microsoft.TestPlatform.TestHost": "18.5.1" } }, "Moq": { @@ -424,22 +208,22 @@ }, "MSTest.TestAdapter": { "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "lZRgNzaQnffK4XLjM/og4Eoqp/3IkpcyJQQcyKXkPdkzCT3+ghpwHa9zG1xYhQDbUFoc54M+/waLwh31K9stDQ==", + "requested": "[4.2.2, )", + "resolved": "4.2.2", + "contentHash": "gMKNPoBnnlYM1DY+zAxJP05LDgXNHkjqxj6QQsm/O71nZh5BJ2SzsaTaQBQhXlu/HjzQ2CCbnMgufU13kYIpVA==", "dependencies": { - "MSTest.TestFramework": "4.2.1", - "Microsoft.Testing.Extensions.VSTestBridge": "2.2.1", - "Microsoft.Testing.Platform.MSBuild": "2.2.1" + "MSTest.TestFramework": "4.2.2", + "Microsoft.Testing.Extensions.VSTestBridge": "2.2.2", + "Microsoft.Testing.Platform.MSBuild": "2.2.2" } }, "MSTest.TestFramework": { "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "I4/RbS2TpGZ56CE98+jPbrGlcerYtw2LvPVKzQGvyQQcJDekPy2Kd+fnThXYn+geJ1sW+vA9B7++rFNxvKcWxA==", + "requested": "[4.2.2, )", + "resolved": "4.2.2", + "contentHash": "IGjOt2kE6NxIgWYcM40DYSzCFaajLe6wHEICPRBnCqj1K4f9HrBLMPo4PE4mM/uKHNgDBvhvj/t1bXenUcQKqQ==", "dependencies": { - "MSTest.Analyzers": "4.2.1" + "MSTest.Analyzers": "4.2.2" } }, "RichardSzalay.MockHttp": { @@ -463,79 +247,117 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" + "resolved": "18.5.1", + "contentHash": "vMFDR1ZjqzzgKmM0zrPie7Gv9Y+ZppjODB5Quzu9Eq0TlIusUfUCYFPEawO91zQuqwzvdFbJSU7WHNtjStffJQ==" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.18.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.18.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.18.0" + } }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "7zB8BjffOyvqfHF26rFVPuK0w1fCf5+j1tLuhHIr76CqxXkGb+fMJtq6YNOV+m6qPytExHMXxluk3RgJ+dSIqw==", + "resolved": "2.2.2", + "contentHash": "qKRghdaDiC88N1s3LDJO7zW74QNZu/ErnTxuG7R9u9UORn6pTwdqbi7X+eY4UQb+7YV2gR2yz8eRelvOWQVxhA==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "RD6D1Jx6cKDA5IHd1H2q8ylIuQG3PD+gdULI0JC8CvsRtaypFzTFpB5xDPuQi8o6kAkcM04cBhAiJPxZboNH2Q==", + "resolved": "2.2.2", + "contentHash": "MuOC3Be70FPysaPxaO0f3GFoXU49UwnKCVDWfFrOZ93h955KZ6MKiJ6vwt/2r4e1wkLDoJFbkQzi/MNbpe4oXQ==", "dependencies": { - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Extensions.VSTestBridge": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "D8AGlkNtlTQPe3zf4SLnHBMr13lerMe0RuHSoRfnRatcuX/T7YbRtgn39rWBjKhXsNio0WXKrPKv3gfWE2I46w==", + "resolved": "2.2.2", + "contentHash": "dyo49lXzY3seyfEgv7qrkIqdvrMAjdJjmY0VDPE//UPK89c+65cqQm8m+FO5XbRpr8gB6AUi5KCRbEl1eRlwQA==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", - "Microsoft.Testing.Extensions.Telemetry": "2.2.1", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.1", - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Extensions.Telemetry": "2.2.2", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.2", + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Platform": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "9bbPuls/b6/vUFzxbSjJLZlJHyKBfOZE5kjIY+ITI2ASqlFPJhR83BdLydJeQOCLEZhEbrEcz5xtt1B69nwSVg==" + "resolved": "2.2.2", + "contentHash": "9mUsTOri0aVqBX7/EJwqVJxVwdOzGUVJqK1H2EMfIl9xxJuSdqhfAlJbukl/iNugvi4+cmQs/LI8PLTDUT9P1A==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "CSJOcZHfKlTyPbS0CTJk6iEnU4gJC+eUA5z72UBnMDRdgVHYOmB8k9Y7jT233gZjnCOQiYFg3acQHRfu2H62nw==", + "resolved": "2.2.2", + "contentHash": "acgkTLYA8C39oe5b5ISmydBshR0XO6v8z3/CXAsLmPQ3xAiomHuPoTAgY28tjQLcwPZOu4GX034BXWvmsVpzIg==", "dependencies": { - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg==" + "resolved": "18.5.1", + "contentHash": "KNZd+M0S0rz5eNAln0pbZX+A/RbokYZCbGKx4fN4CkhtWhkz6nSJDO+9LGYjRE4d0WPVriJ2JnVubkjt3+PpMg==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==", + "resolved": "18.5.1", + "contentHash": "RM+3JNHEoHOCFXzVntUcIiYxzPjzBN0N8wto6HYXi76YyBTZ/3CeRL8U+Pk5zx3AUrOmHxDvKJwGUCdElU9bJg==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.4.0", + "Microsoft.TestPlatform.ObjectModel": "18.5.1", "Newtonsoft.Json": "13.0.3" } }, "MSTest.Analyzers": { "type": "Transitive", - "resolved": "4.2.1", - "contentHash": "1i9jgE/42KGGyZ4s0MdrYM/Uu/dRYhbRfYQifcO0AZ6vw4sBXRjoQGQRGNSm771AYgPAmoGl0u4sJc2lMET6HQ==" + "resolved": "4.2.2", + "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "Portable.BouncyCastle": { - "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", @@ -544,19 +366,20 @@ "webpush": { "type": "Project", "dependencies": { - "Portable.BouncyCastle": "[1.9.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )", + "Microsoft.IdentityModel.Tokens": "[8.18.0, )" } } }, "net9.0": { "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[18.4.0, )", - "resolved": "18.4.0", - "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==", + "requested": "[18.5.1, )", + "resolved": "18.5.1", + "contentHash": "SfqVaLiIqAbRWuPg5BP4QFwBIirQj/YIL8Dhxl6zntBKbXp0cQykoV480SmwG+yRMiWptxEI6NbHQuGSZ8b97w==", "dependencies": { - "Microsoft.CodeCoverage": "18.4.0", - "Microsoft.TestPlatform.TestHost": "18.4.0" + "Microsoft.CodeCoverage": "18.5.1", + "Microsoft.TestPlatform.TestHost": "18.5.1" } }, "Moq": { @@ -570,22 +393,22 @@ }, "MSTest.TestAdapter": { "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "lZRgNzaQnffK4XLjM/og4Eoqp/3IkpcyJQQcyKXkPdkzCT3+ghpwHa9zG1xYhQDbUFoc54M+/waLwh31K9stDQ==", + "requested": "[4.2.2, )", + "resolved": "4.2.2", + "contentHash": "gMKNPoBnnlYM1DY+zAxJP05LDgXNHkjqxj6QQsm/O71nZh5BJ2SzsaTaQBQhXlu/HjzQ2CCbnMgufU13kYIpVA==", "dependencies": { - "MSTest.TestFramework": "4.2.1", - "Microsoft.Testing.Extensions.VSTestBridge": "2.2.1", - "Microsoft.Testing.Platform.MSBuild": "2.2.1" + "MSTest.TestFramework": "4.2.2", + "Microsoft.Testing.Extensions.VSTestBridge": "2.2.2", + "Microsoft.Testing.Platform.MSBuild": "2.2.2" } }, "MSTest.TestFramework": { "type": "Direct", - "requested": "[4.2.1, )", - "resolved": "4.2.1", - "contentHash": "I4/RbS2TpGZ56CE98+jPbrGlcerYtw2LvPVKzQGvyQQcJDekPy2Kd+fnThXYn+geJ1sW+vA9B7++rFNxvKcWxA==", + "requested": "[4.2.2, )", + "resolved": "4.2.2", + "contentHash": "IGjOt2kE6NxIgWYcM40DYSzCFaajLe6wHEICPRBnCqj1K4f9HrBLMPo4PE4mM/uKHNgDBvhvj/t1bXenUcQKqQ==", "dependencies": { - "MSTest.Analyzers": "4.2.1" + "MSTest.Analyzers": "4.2.2" } }, "RichardSzalay.MockHttp": { @@ -609,79 +432,117 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" + "resolved": "18.5.1", + "contentHash": "vMFDR1ZjqzzgKmM0zrPie7Gv9Y+ZppjODB5Quzu9Eq0TlIusUfUCYFPEawO91zQuqwzvdFbJSU7WHNtjStffJQ==" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.18.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.18.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.18.0" + } }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "7zB8BjffOyvqfHF26rFVPuK0w1fCf5+j1tLuhHIr76CqxXkGb+fMJtq6YNOV+m6qPytExHMXxluk3RgJ+dSIqw==", + "resolved": "2.2.2", + "contentHash": "qKRghdaDiC88N1s3LDJO7zW74QNZu/ErnTxuG7R9u9UORn6pTwdqbi7X+eY4UQb+7YV2gR2yz8eRelvOWQVxhA==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "RD6D1Jx6cKDA5IHd1H2q8ylIuQG3PD+gdULI0JC8CvsRtaypFzTFpB5xDPuQi8o6kAkcM04cBhAiJPxZboNH2Q==", + "resolved": "2.2.2", + "contentHash": "MuOC3Be70FPysaPxaO0f3GFoXU49UwnKCVDWfFrOZ93h955KZ6MKiJ6vwt/2r4e1wkLDoJFbkQzi/MNbpe4oXQ==", "dependencies": { - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Extensions.VSTestBridge": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "D8AGlkNtlTQPe3zf4SLnHBMr13lerMe0RuHSoRfnRatcuX/T7YbRtgn39rWBjKhXsNio0WXKrPKv3gfWE2I46w==", + "resolved": "2.2.2", + "contentHash": "dyo49lXzY3seyfEgv7qrkIqdvrMAjdJjmY0VDPE//UPK89c+65cqQm8m+FO5XbRpr8gB6AUi5KCRbEl1eRlwQA==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", - "Microsoft.Testing.Extensions.Telemetry": "2.2.1", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.1", - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Extensions.Telemetry": "2.2.2", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.2", + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.Testing.Platform": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "9bbPuls/b6/vUFzxbSjJLZlJHyKBfOZE5kjIY+ITI2ASqlFPJhR83BdLydJeQOCLEZhEbrEcz5xtt1B69nwSVg==" + "resolved": "2.2.2", + "contentHash": "9mUsTOri0aVqBX7/EJwqVJxVwdOzGUVJqK1H2EMfIl9xxJuSdqhfAlJbukl/iNugvi4+cmQs/LI8PLTDUT9P1A==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "CSJOcZHfKlTyPbS0CTJk6iEnU4gJC+eUA5z72UBnMDRdgVHYOmB8k9Y7jT233gZjnCOQiYFg3acQHRfu2H62nw==", + "resolved": "2.2.2", + "contentHash": "acgkTLYA8C39oe5b5ISmydBshR0XO6v8z3/CXAsLmPQ3xAiomHuPoTAgY28tjQLcwPZOu4GX034BXWvmsVpzIg==", "dependencies": { - "Microsoft.Testing.Platform": "2.2.1" + "Microsoft.Testing.Platform": "2.2.2" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg==" + "resolved": "18.5.1", + "contentHash": "KNZd+M0S0rz5eNAln0pbZX+A/RbokYZCbGKx4fN4CkhtWhkz6nSJDO+9LGYjRE4d0WPVriJ2JnVubkjt3+PpMg==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==", + "resolved": "18.5.1", + "contentHash": "RM+3JNHEoHOCFXzVntUcIiYxzPjzBN0N8wto6HYXi76YyBTZ/3CeRL8U+Pk5zx3AUrOmHxDvKJwGUCdElU9bJg==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.4.0", + "Microsoft.TestPlatform.ObjectModel": "18.5.1", "Newtonsoft.Json": "13.0.3" } }, "MSTest.Analyzers": { "type": "Transitive", - "resolved": "4.2.1", - "contentHash": "1i9jgE/42KGGyZ4s0MdrYM/Uu/dRYhbRfYQifcO0AZ6vw4sBXRjoQGQRGNSm771AYgPAmoGl0u4sJc2lMET6HQ==" + "resolved": "4.2.2", + "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "Portable.BouncyCastle": { - "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", @@ -690,7 +551,8 @@ "webpush": { "type": "Project", "dependencies": { - "Portable.BouncyCastle": "[1.9.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )", + "Microsoft.IdentityModel.Tokens": "[8.18.0, )" } } } diff --git a/WebPush/IWebPushClient.cs b/WebPush/IWebPushClient.cs index 005be35..716d517 100644 --- a/WebPush/IWebPushClient.cs +++ b/WebPush/IWebPushClient.cs @@ -4,114 +4,127 @@ using System.Threading; using System.Threading.Tasks; -namespace WebPush +namespace WebPush; + +public interface IWebPushClient : IDisposable { - public interface IWebPushClient : IDisposable - { - /// - /// When sending messages to a GCM endpoint you need to set the GCM API key - /// by either calling setGcmApiKey() or passing in the API key as an option - /// to sendNotification() - /// - /// The API key to send with the GCM request. - void SetGcmApiKey(string gcmApiKey); + /// + /// When marking requests where you want to define VAPID details, call this method + /// before sendNotifications() or pass in the details and options to + /// sendNotification. + /// + /// + void SetVapidDetails(VapidDetails vapidDetails); - /// - /// When marking requests where you want to define VAPID details, call this method - /// before sendNotifications() or pass in the details and options to - /// sendNotification. - /// - /// - void SetVapidDetails(VapidDetails vapidDetails); + /// + /// When marking requests where you want to define VAPID details, call this method + /// before sendNotifications() or pass in the details and options to + /// sendNotification. + /// + /// This must be either a URL or a 'mailto:' address + /// The public VAPID key as a base64 encoded string + /// The private VAPID key as a base64 encoded string + void SetVapidDetails(string subject, string publicKey, string privateKey); - /// - /// When marking requests where you want to define VAPID details, call this method - /// before sendNotifications() or pass in the details and options to - /// sendNotification. - /// - /// This must be either a URL or a 'mailto:' address - /// The public VAPID key as a base64 encoded string - /// The private VAPID key as a base64 encoded string - void SetVapidDetails(string subject, string publicKey, string privateKey); + /// + /// To get a request without sending a push notification call this method. + /// This method will throw an ArgumentException if there is an issue with the input. + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for vapid keys can be passed in if they are unique for each + /// notification. + /// + /// A HttpRequestMessage object that can be sent. + [Obsolete("Use GenerateRequestDetails with WebPushOptions")] + HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? payload, + Dictionary? options = null); - /// - /// To get a request without sending a push notification call this method. - /// This method will throw an ArgumentException if there is an issue with the input. - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// - /// Options for the GCM API key and vapid keys can be passed in if they are unique for each - /// notification. - /// - /// A HttpRequestMessage object that can be sent. - HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string payload, - Dictionary options = null); + /// + /// To get a request without sending a push notification call this method. + /// This method will throw an ArgumentException if there is an issue with the input. + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for the web push request, including vapid keys, if they are unique for each + /// notification. + /// + /// A HttpRequestMessage object that can be sent. + public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? payload, WebPushOptions? options = null); - /// - /// To send a push notification call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// - /// Options for the GCM API key and vapid keys can be passed in if they are unique for each - /// notification. - /// - void SendNotification(PushSubscription subscription, string payload = null, - Dictionary options = null); + /// + /// To send a push notification call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for the web push request, including vapid keys, if they are unique for each + /// notification. + /// + void SendNotification(PushSubscription subscription, string? payload = null, WebPushOptions? options = null); - /// - /// To send a push notification call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// The vapid details for the notification. - void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails); + /// + /// To send a push notification call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for vapid keys can be passed in if they are unique for each + /// notification. + /// + [Obsolete("Use SendNotification with WebPushOptions")] + void SendNotification(PushSubscription subscription, string? payload = null, + Dictionary? options = null); - /// - /// To send a push notification call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// The GCM API key - void SendNotification(PushSubscription subscription, string payload, string gcmApiKey); + /// + /// To send a push notification call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// The vapid details for the notification. + void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails); - /// - /// To send a push notification asynchronous call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// - /// Options for the GCM API key and vapid keys can be passed in if they are unique for each - /// notification. - /// - /// The cancellation token to cancel operation. - Task SendNotificationAsync(PushSubscription subscription, string payload = null, - Dictionary options = null, CancellationToken cancellationToken=default); + /// + /// To send a push notification asynchronous call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for the web push request, including vapid keys, if they are unique for each + /// notification. + /// + /// The cancellation token to cancel operation. + Task SendNotificationAsync(PushSubscription subscription, string? payload = null, WebPushOptions? options = null, CancellationToken cancellationToken = default); - /// - /// To send a push notification asynchronous call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// The vapid details for the notification. - /// - Task SendNotificationAsync(PushSubscription subscription, string payload, - VapidDetails vapidDetails, CancellationToken cancellationToken=default); + /// + /// To send a push notification asynchronous call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for vapid keys can be passed in if they are unique for each + /// notification. + /// + /// The cancellation token to cancel operation. + [Obsolete("Use SendNotificationAsync with WebPushOptions")] + Task SendNotificationAsync(PushSubscription subscription, string? payload = null, + Dictionary? options = null, CancellationToken cancellationToken = default); - /// - /// To send a push notification asynchronous call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// The GCM API key - /// - Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey, CancellationToken cancellationToken=default); - } + /// + /// To send a push notification asynchronous call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// The vapid details for the notification. + /// + Task SendNotificationAsync(PushSubscription subscription, string payload, + VapidDetails vapidDetails, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/WebPush/Model/ContentEncoding.cs b/WebPush/Model/ContentEncoding.cs new file mode 100644 index 0000000..27a58e2 --- /dev/null +++ b/WebPush/Model/ContentEncoding.cs @@ -0,0 +1,7 @@ +namespace WebPush; + +public enum ContentEncoding +{ + Aesgcm, + Aes128gcm, +} diff --git a/WebPush/Model/EncryptionResult.cs b/WebPush/Model/EncryptionResult.cs index 1b5e9a7..2fed814 100644 --- a/WebPush/Model/EncryptionResult.cs +++ b/WebPush/Model/EncryptionResult.cs @@ -1,23 +1,22 @@ -using WebPush.Util; +using Microsoft.IdentityModel.Tokens; -namespace WebPush +namespace WebPush.Model; + +// @LogicSoftware +// Originally From: https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/EncryptionResult.cs +public class EncryptionResult { - // @LogicSoftware - // Originally From: https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/EncryptionResult.cs - public class EncryptionResult - { - public byte[] PublicKey { get; set; } - public byte[] Payload { get; set; } - public byte[] Salt { get; set; } + public required byte[] PublicKey { get; set; } + public required byte[] Payload { get; set; } + public required byte[] Salt { get; set; } - public string Base64EncodePublicKey() - { - return UrlBase64.Encode(PublicKey); - } + public string Base64EncodePublicKey() + { + return Base64UrlEncoder.Encode(PublicKey); + } - public string Base64EncodeSalt() - { - return UrlBase64.Encode(Salt); - } + public string Base64EncodeSalt() + { + return Base64UrlEncoder.Encode(Salt); } -} \ No newline at end of file +} diff --git a/WebPush/Model/InvalidEncryptionDetailsException.cs b/WebPush/Model/InvalidEncryptionDetailsException.cs index 814b379..dd46852 100644 --- a/WebPush/Model/InvalidEncryptionDetailsException.cs +++ b/WebPush/Model/InvalidEncryptionDetailsException.cs @@ -1,15 +1,14 @@ using System; -namespace WebPush.Model +namespace WebPush.Model; + +public class InvalidEncryptionDetailsException : Exception { - public class InvalidEncryptionDetailsException : Exception + public InvalidEncryptionDetailsException(string message, PushSubscription pushSubscription) + : base(message) { - public InvalidEncryptionDetailsException(string message, PushSubscription pushSubscription) - : base(message) - { - PushSubscription = pushSubscription; - } - - public PushSubscription PushSubscription { get; } + PushSubscription = pushSubscription; } + + public PushSubscription PushSubscription { get; } } diff --git a/WebPush/Model/PushSubscription.cs b/WebPush/Model/PushSubscription.cs index 98587c0..8816f73 100644 --- a/WebPush/Model/PushSubscription.cs +++ b/WebPush/Model/PushSubscription.cs @@ -1,20 +1,15 @@ -namespace WebPush +namespace WebPush; + +public class PushSubscription { - public class PushSubscription + public PushSubscription(string endpoint, string p256dh, string auth) { - public PushSubscription() - { - } - - public PushSubscription(string endpoint, string p256dh, string auth) - { - Endpoint = endpoint; - P256DH = p256dh; - Auth = auth; - } - - public string Endpoint { get; set; } - public string P256DH { get; set; } - public string Auth { get; set; } + Endpoint = endpoint; + P256DH = p256dh; + Auth = auth; } -} \ No newline at end of file + + public string Endpoint { get; set; } + public string P256DH { get; set; } + public string Auth { get; set; } +} diff --git a/WebPush/Model/VapidDetails.cs b/WebPush/Model/VapidDetails.cs index 1478216..82e5e89 100644 --- a/WebPush/Model/VapidDetails.cs +++ b/WebPush/Model/VapidDetails.cs @@ -1,25 +1,23 @@ -namespace WebPush -{ - public class VapidDetails - { - public VapidDetails() - { - } - - /// This should be a URL or a 'mailto:' email address - /// The VAPID public key as a base64 encoded string - /// The VAPID private key as a base64 encoded string - public VapidDetails(string subject, string publicKey, string privateKey) - { - Subject = subject; - PublicKey = publicKey; - PrivateKey = privateKey; - } +using System; +using System.Diagnostics.CodeAnalysis; - public string Subject { get; set; } - public string PublicKey { get; set; } - public string PrivateKey { get; set; } +namespace WebPush; - public long Expiration { get; set; } = -1; +public class VapidDetails +{ + /// This should be a URL or a 'mailto:' email address + /// The VAPID public key as a base64 encoded string + /// The VAPID private key as a base64 encoded string + [SetsRequiredMembers] + public VapidDetails(string subject, string publicKey, string privateKey) + { + Subject = subject; + PublicKey = publicKey; + PrivateKey = privateKey; } -} \ No newline at end of file + + public required string Subject { get; set; } + public required string PublicKey { get; set; } + public required string PrivateKey { get; set; } + public DateTime? Expiration { get; set; } = null; +} diff --git a/WebPush/Model/WebPushException.cs b/WebPush/Model/WebPushException.cs index 6fa5cb6..86b977d 100644 --- a/WebPush/Model/WebPushException.cs +++ b/WebPush/Model/WebPushException.cs @@ -3,20 +3,19 @@ using System.Net.Http; using System.Net.Http.Headers; -namespace WebPush +namespace WebPush.Model; + +public class WebPushException : Exception { - public class WebPushException : Exception + public WebPushException(string message, PushSubscription pushSubscription, HttpResponseMessage responseMessage) : base(message) { - public WebPushException(string message, PushSubscription pushSubscription, HttpResponseMessage responseMessage) : base(message) - { - PushSubscription = pushSubscription; - HttpResponseMessage = responseMessage; - } + PushSubscription = pushSubscription; + HttpResponseMessage = responseMessage; + } - public HttpStatusCode StatusCode => HttpResponseMessage.StatusCode; + public HttpStatusCode StatusCode => HttpResponseMessage.StatusCode; - public HttpResponseHeaders Headers => HttpResponseMessage.Headers; - public PushSubscription PushSubscription { get; set; } - public HttpResponseMessage HttpResponseMessage { get; set; } - } -} \ No newline at end of file + public HttpResponseHeaders Headers => HttpResponseMessage.Headers; + public PushSubscription PushSubscription { get; set; } + public HttpResponseMessage HttpResponseMessage { get; set; } +} diff --git a/WebPush/Model/WebPushOptions.cs b/WebPush/Model/WebPushOptions.cs new file mode 100644 index 0000000..3b9cbda --- /dev/null +++ b/WebPush/Model/WebPushOptions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace WebPush; + +public class WebPushOptions +{ + public VapidDetails? VapidDetails { get; set; } + public const int DefaultTtl = 2419200; // default is 4 weeks + public int TTL { get; set; } = DefaultTtl; + public const ContentEncoding DefaultContentEncoding = WebPush.ContentEncoding.Aes128gcm; + public ContentEncoding? ContentEncoding { get; set; } + public Urgency? Urgency { get; set; } + public string? Topic { get; set; } + public Dictionary? ExtraHeaders { get; set; } + + // 'proxy', + // 'agent', + // 'timeout' +} diff --git a/WebPush/Urgency.cs b/WebPush/Urgency.cs new file mode 100644 index 0000000..dc59019 --- /dev/null +++ b/WebPush/Urgency.cs @@ -0,0 +1,9 @@ +namespace WebPush; + +public enum Urgency +{ + VeryLow, + Low, + Normal, + High, +} diff --git a/WebPush/Util/ECKeyHelper.cs b/WebPush/Util/ECKeyHelper.cs index 5655908..af809f7 100644 --- a/WebPush/Util/ECKeyHelper.cs +++ b/WebPush/Util/ECKeyHelper.cs @@ -1,64 +1,78 @@ 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.Linq; +using System.Security.Cryptography; +using Microsoft.IdentityModel.Tokens; -namespace WebPush.Util +namespace WebPush.Util; + +internal static class ECKeyHelper { - internal static class ECKeyHelper + public static ECDsa GetKeyPair(byte[] privateKey, byte[] publicKey) { - public static ECPrivateKeyParameters GetPrivateKey(byte[] privateKey) + return ECDsa.Create(new ECParameters { - Asn1Object version = new DerInteger(1); - Asn1Object derEncodedKey = new DerOctetString(privateKey); - Asn1Object keyTypeParameters = new DerTaggedObject(0, new DerObjectIdentifier(@"1.2.840.10045.3.1.7")); - - Asn1Object derSequence = new DerSequence(version, derEncodedKey, keyTypeParameters); - - var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); - var pemKey = "-----BEGIN EC PRIVATE KEY-----\n"; - pemKey += base64EncodedDerSequence; - pemKey += "\n-----END EC PRIVATE KEY----"; + Curve = ECCurve.NamedCurves.nistP256, + D = privateKey, + Q = new ECPoint + { + X = [.. publicKey.Skip(1).Take(32)], + Y = [.. publicKey.Skip(33)], + } + }); + } - var reader = new StringReader(pemKey); - var pemReader = new PemReader(reader); - var keyPair = (AsymmetricCipherKeyPair) pemReader.ReadObject(); + public static byte[] GetPublicKey(this ECDsa keypair) + { + var ep = keypair.ExportParameters(includePrivateParameters: true); + if (ep.Q.X is null || ep.Q.Y is null) throw new InvalidOperationException(); + return [4, .. ep.Q.X, .. ep.Q.Y]; + } - return (ECPrivateKeyParameters) keyPair.Private; - } - 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); + public static byte[] GetPrivateKey(this ECDsa keypair) + { + var ep = keypair.ExportParameters(includePrivateParameters: true); + if (ep.D is null) throw new InvalidOperationException(); + return [.. ep.D]; + } - Asn1Object derSequence = new DerSequence(keyTypeParameters, derEncodedKey); + public static string GetEncodedPublicKey(this ECDsa keypair) => Base64UrlEncoder.Encode(keypair.GetPublicKey()); + public static string GetEncodedPrivateKey(this ECDsa keypair) => Base64UrlEncoder.Encode(keypair.GetPrivateKey()); - var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); - var pemKey = "-----BEGIN PUBLIC KEY-----\n"; - pemKey += base64EncodedDerSequence; - pemKey += "\n-----END PUBLIC KEY-----"; + public static ECDsa GenerateKeys() + { + return ECDsa.Create(ECCurve.NamedCurves.nistP256); + } - var reader = new StringReader(pemKey); - var pemReader = new PemReader(reader); - var keyPair = pemReader.ReadObject(); - return (ECPublicKeyParameters) keyPair; - } + public static byte[] GetECDiffieHellmanSharedKey(byte[] privateKey, byte[] publicKey) + { + var myKey = CreateWithPrivateKey(privateKey); + var otherKey = CreateWithPublicKey(publicKey); + return myKey.DeriveRawSecretAgreement(otherKey.PublicKey); + } - public static AsymmetricCipherKeyPair GenerateKeys() + internal static ECDiffieHellman CreateWithPrivateKey(byte[] privateKey) + { + var parameters = new ECParameters { - 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())); + Curve = ECCurve.NamedCurves.nistP256, + D = privateKey, + }; + return ECDiffieHellman.Create(parameters); + } - return keyPairGenerator.GenerateKeyPair(); - } + internal static ECDiffieHellman CreateWithPublicKey(byte[] publicKey) + { + var parameters = new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + Q = new ECPoint + { + X = [.. publicKey.Skip(1).Take(32)], + Y = [.. publicKey.Skip(33)], + }, + }; + return ECDiffieHellman.Create(parameters); } -} \ No newline at end of file + +} diff --git a/WebPush/Util/Encryptor.cs b/WebPush/Util/Encryptor.cs index d8b294f..ed1d3ea 100644 --- a/WebPush/Util/Encryptor.cs +++ b/WebPush/Util/Encryptor.cs @@ -1,150 +1,105 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.Runtime.CompilerServices; +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; - -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); - } +using Microsoft.IdentityModel.Tokens; +using WebPush.Model; - public static EncryptionResult Encrypt(byte[] userKey, byte[] userSecret, byte[] payload) - { - var salt = GenerateSalt(16); - var serverKeyPair = ECKeyHelper.GenerateKeys(); +[assembly: InternalsVisibleTo("WebPush.Test")] - var ecdhAgreement = AgreementUtilities.GetBasicAgreement("ECDH"); - ecdhAgreement.Init(serverKeyPair.Private); +namespace WebPush.Util; - var userPublicKey = ECKeyHelper.GetPublicKey(userKey); +internal static class Encryptor +{ + public static EncryptionResult Encrypt(string subscriptionPublicKeyBase64, string authSecretBase64, string payload) + { + var subscriptionPublicKey = Base64UrlEncoder.DecodeBytes(subscriptionPublicKeyBase64); + var authenticationSecret = Base64UrlEncoder.DecodeBytes(authSecretBase64); - var key = ecdhAgreement.CalculateAgreement(userPublicKey).ToByteArrayUnsigned(); - var serverPublicKey = ((ECPublicKeyParameters) serverKeyPair.Public).Q.GetEncoded(false); + // see https://datatracker.ietf.org/doc/html/rfc8291 - 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); + using var ephemeralEcdh = ECKeyHelper.GenerateKeys(); + var uncompressedEphemeralPublicKey = ephemeralEcdh.GetPublicKey(); + var sharedSecret = ECKeyHelper.GetECDiffieHellmanSharedKey(ephemeralEcdh.GetPrivateKey(), subscriptionPublicKey); - var input = AddPaddingToInput(payload); - var encryptedMessage = EncryptAes(nonce, cek, input); + Span salt = stackalloc byte[16]; + RandomNumberGenerator.Fill(salt); - return new EncryptionResult - { - Salt = salt, - Payload = encryptedMessage, - PublicKey = serverPublicKey - }; - } + // Step 0 PRK_key + Span prkkey = stackalloc byte[32]; // SHA256 output is 32 bytes + HKDF.Extract(HashAlgorithmName.SHA256, sharedSecret, authenticationSecret, prkkey); - private static byte[] GenerateSalt(int length) - { - var salt = new byte[length]; - var random = new Random(); - random.NextBytes(salt); - return salt; - } + // Step 1 IKM + byte[] keyInfo = [.. Encoding.UTF8.GetBytes("WebPush: info"), 0x00, .. subscriptionPublicKey, .. uncompressedEphemeralPublicKey]; + Span ikm = stackalloc byte[32]; + HKDF.Expand(HashAlgorithmName.SHA256, prkkey, ikm, keyInfo); - 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; - } + // Step 2 PRK + Span prk = stackalloc byte[32]; + HKDF.Extract(HashAlgorithmName.SHA256, ikm, salt, prk); - 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); + // Step 3 CEK + byte[] cekInfo = [.. Encoding.UTF8.GetBytes("Content-Encoding: aes128gcm"), 0x00]; + Span cek = stackalloc byte[16]; + HKDF.Expand(HashAlgorithmName.SHA256, prk, cek, cekInfo); - //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); + // Step 4 NONCE + byte[] nonceInfo = [.. Encoding.UTF8.GetBytes("Content-Encoding: nonce"), 0x00]; + Span nonce = stackalloc byte[12]; + HKDF.Expand(HashAlgorithmName.SHA256, prk, nonce, nonceInfo); - //byte[] tag = cipher.GetMac(); - return cipherText; - } + // Step 5 Header + var maxContentLength = BitConverter.GetBytes(Convert.ToInt32(4096)); + if (BitConverter.IsLittleEndian) { Array.Reverse(maxContentLength); } + var asPublicLength = Convert.ToByte(uncompressedEphemeralPublicKey.Length); + byte[] header = [.. salt, .. maxContentLength, asPublicLength, .. uncompressedEphemeralPublicKey]; - 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); + // Step 6 Payload padding + byte[] paddedPayload = [.. Encoding.UTF8.GetBytes(payload), 0x02]; - if (result.Length > length) - { - Array.Resize(ref result, length); - } + // Step 7 Content Encryption + var cipherText = EncryptMessage(paddedPayload, [.. cek], [.. nonce]); + byte[] encryptedContent = [.. header, .. cipherText]; - 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) + return new EncryptionResult { - 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(); - } + Salt = [.. salt], + Payload = encryptedContent, + PublicKey = uncompressedEphemeralPublicKey + }; } - public class HmacSha256 + /// + /// Encrypts a byte array using AES with a given key and a new random IV. + /// + /// The Web Push protocol specifies a 16-byte authentication tag. + /// + public static byte[] EncryptMessage(byte[] payload, byte[] key, byte[] iv) { - private readonly HMac _hmac; + var tag = new byte[AesGcm.TagByteSizes.MaxSize]; + var encryptedBytes = new byte[payload.Length]; - public HmacSha256(byte[] key) + using (var aesGcm = new AesGcm(key, AesGcm.TagByteSizes.MaxSize)) { - _hmac = new HMac(new Sha256Digest()); - _hmac.Init(new KeyParameter(key)); + aesGcm.Encrypt(iv, payload, encryptedBytes, tag); } - public byte[] ComputeHash(byte[] value) - { - var resBuf = new byte[_hmac.GetMacSize()]; - _hmac.BlockUpdate(value, 0, value.Length); - _hmac.DoFinal(resBuf, 0); + return [.. encryptedBytes, .. tag]; + } - return resBuf; - } + /// + /// Decrypts a byte array using AES with a given key and IV. + /// + /// ciphertext must contain the tag as the end (last 16 bytes). + /// + public static string DecryptMessage(byte[] payload, byte[] key, byte[] nonce) + { + ReadOnlySpan readOnlySpan = payload; + var tag = readOnlySpan.Slice(payload.Length - AesGcm.TagByteSizes.MaxSize, length: AesGcm.TagByteSizes.MaxSize); + var ciphertext = readOnlySpan.Slice(0, payload.Length - AesGcm.TagByteSizes.MaxSize); + using var aes = new AesGcm(key, tag.Length); + var plaintextBytes = new byte[ciphertext.Length]; + aes.Decrypt(nonce, ciphertext, tag, plaintextBytes); + return Encoding.UTF8.GetString(plaintextBytes); } -} \ No newline at end of file +} diff --git a/WebPush/Util/EnumHelper.cs b/WebPush/Util/EnumHelper.cs new file mode 100644 index 0000000..0739a4c --- /dev/null +++ b/WebPush/Util/EnumHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace WebPush.Util; + +public static partial class EnumHelper2 +{ + + public static string ToKebabCaseLower(this T val) where T : Enum + { + return RegexVariableName().Replace(val.ToString()!, "${first}-${remainder}").ToLower(CultureInfo.InvariantCulture); + } + + [GeneratedRegex("(?[a-z0-9]|(?<=[a-z0-9]))(?[A-Z])", RegexOptions.None, matchTimeoutMilliseconds: 200)] + private static partial Regex RegexVariableName(); +} + diff --git a/WebPush/Util/JwsSigner.cs b/WebPush/Util/JwsSigner.cs deleted file mode 100644 index f7aaf52..0000000 --- a/WebPush/Util/JwsSigner.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.Json; -using Org.BouncyCastle.Crypto.Digests; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Crypto.Signers; - -namespace WebPush.Util -{ - internal class JwsSigner - { - private readonly ECPrivateKeyParameters _privateKey; - - public JwsSigner(ECPrivateKeyParameters privateKey) - { - _privateKey = privateKey; - } - - /// - /// Generates a Jws Signature. - /// - /// - /// - /// - public string GenerateSignature(Dictionary header, Dictionary payload) - { - var securedInput = SecureInput(header, payload); - var message = Encoding.UTF8.GetBytes(securedInput); - - var hashedMessage = Sha256Hash(message); - - var signer = new ECDsaSigner(); - signer.Init(true, _privateKey); - var results = signer.GenerateSignature(hashedMessage); - - // Concated to create signature - var a = results[0].ToByteArrayUnsigned(); - var b = results[1].ToByteArrayUnsigned(); - - // a,b are required to be exactly the same length of bytes - if (a.Length != b.Length) - { - var largestLength = Math.Max(a.Length, b.Length); - a = ByteArrayPadLeft(a, largestLength); - b = ByteArrayPadLeft(b, largestLength); - } - - var signature = UrlBase64.Encode(a.Concat(b).ToArray()); - return $"{securedInput}.{signature}"; - } - - private static string SecureInput(Dictionary header, Dictionary payload) - { - var encodeHeader = UrlBase64.Encode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(header))); - var encodePayload = UrlBase64.Encode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload))); - - return $"{encodeHeader}.{encodePayload}"; - } - - private static byte[] ByteArrayPadLeft(byte[] src, int size) - { - var dst = new byte[size]; - var startAt = dst.Length - src.Length; - Array.Copy(src, 0, dst, startAt, src.Length); - 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; - } - } -} diff --git a/WebPush/Util/UrlBase64.cs b/WebPush/Util/UrlBase64.cs deleted file mode 100644 index 1e61563..0000000 --- a/WebPush/Util/UrlBase64.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace WebPush.Util -{ - internal static class UrlBase64 - { - /// - /// Decodes a url-safe base64 string into bytes - /// - /// - /// - public static byte[] Decode(string base64) - { - base64 = base64.Replace('-', '+').Replace('_', '/'); - - while (base64.Length % 4 != 0) - { - base64 += "="; - } - - return Convert.FromBase64String(base64); - } - - /// - /// Encodes bytes into url-safe base64 string - /// - /// - /// - public static string Encode(byte[] data) - { - return Convert.ToBase64String(data).Replace('+', '-').Replace('/', '_').TrimEnd('='); - } - } -} \ No newline at end of file diff --git a/WebPush/VapidHelper.cs b/WebPush/VapidHelper.cs index 56d1276..10b77ac 100644 --- a/WebPush/VapidHelper.cs +++ b/WebPush/VapidHelper.cs @@ -1,166 +1,143 @@ using System; using System.Collections.Generic; -using Org.BouncyCastle.Crypto.Parameters; +using System.Security.Claims; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; using WebPush.Util; -namespace WebPush +namespace WebPush; + +public static class VapidHelper { - public static class VapidHelper + /// + /// Generate vapid keys + /// + public static VapidDetails GenerateVapidKeys() { - /// - /// Generate vapid keys - /// - 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(); - - results.PublicKey = UrlBase64.Encode(publicKey); - results.PrivateKey = UrlBase64.Encode(ByteArrayPadLeft(privateKey, 32)); + var keys = ECKeyHelper.GenerateKeys(); + return new VapidDetails("", keys.GetEncodedPublicKey(), keys.GetEncodedPrivateKey()); + } - return results; + /// + /// This method takes the required VAPID parameters and returns the required + /// header to be added to a Web Push Protocol Request. + /// + /// This must be the origin of the push service. + /// This should be a URL or a 'mailto:' email address + /// The VAPID public key as a base64 encoded string + /// The VAPID private key as a base64 encoded string + /// The expiration of the VAPID JWT. + /// A dictionary of header key/value pairs. + public static Dictionary GetVapidHeaders(string audience, string subject, string publicKey, string privateKey, DateTime? expiration = null, ContentEncoding contentEncoding = ContentEncoding.Aes128gcm) + { + ValidateAudience(audience); + ValidateSubject(subject); + ValidatePublicKey(publicKey); + ValidatePrivateKey(privateKey); + var now = DateTime.UtcNow; + if (expiration is null) + { + expiration = now.AddHours(12); } - - /// - /// This method takes the required VAPID parameters and returns the required - /// header to be added to a Web Push Protocol Request. - /// - /// This must be the origin of the push service. - /// This should be a URL or a 'mailto:' email address - /// The VAPID public key as a base64 encoded string - /// The VAPID private key as a base64 encoded string - /// The expiration of the VAPID JWT. - /// A dictionary of header key/value pairs. - public static Dictionary GetVapidHeaders(string audience, string subject, string publicKey, - string privateKey, long expiration = -1) + else { - ValidateAudience(audience); - ValidateSubject(subject); - ValidatePublicKey(publicKey); - ValidatePrivateKey(privateKey); - - var decodedPrivateKey = UrlBase64.Decode(privateKey); - - if (expiration == -1) - { - expiration = UnixTimeNow() + 43200; - } - else - { - ValidateExpiration(expiration); - } - - - var header = new Dictionary {{"typ", "JWT"}, {"alg", "ES256"}}; - - var jwtPayload = new Dictionary {{"aud", audience}, {"exp", expiration}, {"sub", subject}}; - - var signingKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); - - var signer = new JwsSigner(signingKey); - var token = signer.GenerateSignature(header, jwtPayload); - - var results = new Dictionary - { - {"Authorization", "WebPush " + token}, {"Crypto-Key", "p256ecdsa=" + publicKey} - }; - - return results; + ValidateExpiration(expiration); } - public static void ValidateAudience(string audience) + var key = ECKeyHelper.GetKeyPair(Base64UrlEncoder.DecodeBytes(privateKey), Base64UrlEncoder.DecodeBytes(publicKey)); + var identity = new ClaimsIdentity([new Claim(JwtRegisteredClaimNames.Sub, subject)]); + var handler = new JsonWebTokenHandler + { + SetDefaultTimesOnTokenCreation = false, + }; + string token = handler.CreateToken(new SecurityTokenDescriptor + { + Audience = audience, + Expires = expiration, + // IssuedAt = now, + Subject = identity, + SigningCredentials = new SigningCredentials(new ECDsaSecurityKey(key), SecurityAlgorithms.EcdsaSha256), + }); + return contentEncoding switch { - if (string.IsNullOrEmpty(audience)) + ContentEncoding.Aesgcm => new Dictionary(StringComparer.Ordinal) { - throw new ArgumentException(@"No audience could be generated for VAPID."); - } - - if (audience.Length == 0) + { "Authorization", $"WebPush {token}"}, + { "Crypto-Key", $"p256ecdsa={publicKey}"}, + }, + ContentEncoding.Aes128gcm => new Dictionary(StringComparer.Ordinal) { - throw new ArgumentException( - @"The audience value must be a string containing the origin of a push service. " + audience); - } + { "Authorization", $"vapid t={token}, k={publicKey}"}, + }, + _ => throw new Exception("This content encoding is not supported"), + }; + } - if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute)) - { - throw new ArgumentException(@"VAPID audience is not a url."); - } + public static void ValidateAudience(string audience) + { + if (string.IsNullOrWhiteSpace(audience)) + { + throw new ArgumentException( + @$"The audience value must be a string containing the origin of a push service: {audience}", nameof(audience)); } - public static void ValidateSubject(string subject) + if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute)) { - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException(@"A subject is required"); - } - - if (subject.Length == 0) - { - throw new ArgumentException(@"The subject value must be a string containing a url or mailto: address."); - } - - if (!subject.StartsWith("mailto:")) - { - if (!Uri.IsWellFormedUriString(subject, UriKind.Absolute)) - { - throw new ArgumentException(@"Subject is not a valid URL or mailto address"); - } - } + throw new ArgumentException(@$"VAPID audience is not a url: {audience}", nameof(audience)); } + } - public static void ValidatePublicKey(string publicKey) + public static void ValidateSubject(string subject) + { + if (string.IsNullOrWhiteSpace(subject)) { - if (string.IsNullOrEmpty(publicKey)) - { - throw new ArgumentException(@"Valid public key not set"); - } - - var decodedPublicKey = UrlBase64.Decode(publicKey); + throw new ArgumentException(@"The subject value must be a string containing a url or mailto: address.", nameof(subject)); + } - if (decodedPublicKey.Length != 65) + if (!subject.StartsWith("mailto:", StringComparison.Ordinal)) + { + if (!Uri.IsWellFormedUriString(subject, UriKind.Absolute)) { - throw new ArgumentException(@"Vapid public key must be 65 characters long when decoded"); + throw new ArgumentException(@"Subject is not a valid URL or mailto address", nameof(subject)); } } + } - public static void ValidatePrivateKey(string privateKey) + public static void ValidatePublicKey(string publicKey) + { + if (string.IsNullOrWhiteSpace(publicKey)) { - if (string.IsNullOrEmpty(privateKey)) - { - throw new ArgumentException(@"Valid private key not set"); - } + throw new ArgumentException(@"Valid public key not set", nameof(publicKey)); + } - var decodedPrivateKey = UrlBase64.Decode(privateKey); + var decodedPublicKey = Base64UrlEncoder.DecodeBytes(publicKey); - if (decodedPrivateKey.Length != 32) - { - throw new ArgumentException(@"Vapid private key should be 32 bytes long when decoded."); - } + if (decodedPublicKey.Length != 65) + { + throw new ArgumentException(@"Vapid public key must be 65 characters long when decoded", nameof(publicKey)); } + } - private static void ValidateExpiration(long expiration) + public static void ValidatePrivateKey(string privateKey) + { + if (string.IsNullOrWhiteSpace(privateKey)) { - if (expiration <= UnixTimeNow()) - { - throw new ArgumentException(@"Vapid expiration must be a unix timestamp in the future"); - } + throw new ArgumentException(@"Valid private key not set", nameof(privateKey)); } - private static long UnixTimeNow() + var decodedPrivateKey = Base64UrlEncoder.DecodeBytes(privateKey); + + if (decodedPrivateKey.Length != 32) { - var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0); - return (long) timeSpan.TotalSeconds; + throw new ArgumentException(@"Vapid private key should be 32 bytes long when decoded.", nameof(privateKey)); } + } - private static byte[] ByteArrayPadLeft(byte[] src, int size) + private static void ValidateExpiration(DateTime? expiration) + { + if (expiration is null || expiration <= DateTime.UtcNow) { - var dst = new byte[size]; - var startAt = dst.Length - src.Length; - Array.Copy(src, 0, dst, startAt, src.Length); - return dst; + throw new ArgumentException(@"Vapid expiration must be in the future", nameof(expiration)); } } -} \ No newline at end of file +} diff --git a/WebPush/WebPush.csproj b/WebPush/WebPush.csproj index 191634d..2315ca1 100755 --- a/WebPush/WebPush.csproj +++ b/WebPush/WebPush.csproj @@ -1,9 +1,9 @@  - net48;netstandard2.1;net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0 true - 1.0.13 + 2.0.0 Cory Thompson @@ -14,23 +14,18 @@ web push notifications vapid true true + enable + true snupkg README.md - + + - - - - - - - - diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs index df1bfe1..5782675 100644 --- a/WebPush/WebPushClient.cs +++ b/WebPush/WebPushClient.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using WebPush.Model; @@ -11,231 +14,194 @@ [assembly: InternalsVisibleTo("WebPush.Test")] -namespace WebPush +namespace WebPush; + +public partial class WebPushClient : IWebPushClient { - public class WebPushClient : IWebPushClient - { - // default TTL is 4 weeks. - private const int DefaultTtl = 2419200; - private readonly HttpClientHandler _httpClientHandler; + private readonly HttpClientHandler? _httpClientHandler; - private string _gcmApiKey; - private HttpClient _httpClient; - private VapidDetails _vapidDetails; + private HttpClient? _httpClient; + private VapidDetails? _vapidDetails; - // Used so we only cleanup internally created http clients - private bool _isHttpClientInternallyCreated; + // Used so we only cleanup internally created http clients + private bool _isHttpClientInternallyCreated; - public WebPushClient() - { + public WebPushClient() + { - } + } - public WebPushClient(HttpClient httpClient) - { - _httpClient = httpClient; - } + public WebPushClient(HttpClient httpClient) + { + _httpClient = httpClient; + } - public WebPushClient(HttpClientHandler httpClientHandler) - { - _httpClientHandler = httpClientHandler; - } + public WebPushClient(HttpClientHandler httpClientHandler) + { + _httpClientHandler = httpClientHandler; + } - protected HttpClient HttpClient + protected HttpClient HttpClient + { + get { - get + if (_httpClient != null) { - if (_httpClient != null) - { - return _httpClient; - } - - _isHttpClientInternallyCreated = true; - _httpClient = _httpClientHandler == null - ? new HttpClient() - : new HttpClient(_httpClientHandler); - return _httpClient; } + + _isHttpClientInternallyCreated = true; + _httpClient = _httpClientHandler == null + ? new HttpClient() + : new HttpClient(_httpClientHandler); + + return _httpClient; } + } - /// - /// When sending messages to a GCM endpoint you need to set the GCM API key - /// by either calling setGcmApiKey() or passing in the API key as an option - /// to sendNotification() - /// - /// The API key to send with the GCM request. - public void SetGcmApiKey(string gcmApiKey) - { - if (gcmApiKey == null) - { - _gcmApiKey = null; - return; - } + /// + /// When marking requests where you want to define VAPID details, call this method + /// before sendNotifications() or pass in the details and options to + /// sendNotification. + /// + /// + public void SetVapidDetails(VapidDetails vapidDetails) + { + VapidHelper.ValidateSubject(vapidDetails.Subject); + VapidHelper.ValidatePublicKey(vapidDetails.PublicKey); + VapidHelper.ValidatePrivateKey(vapidDetails.PrivateKey); - if (string.IsNullOrEmpty(gcmApiKey)) - { - throw new ArgumentException(@"The GCM API Key should be a non-empty string or null."); - } + _vapidDetails = vapidDetails; + } - _gcmApiKey = gcmApiKey; - } + /// + /// When marking requests where you want to define VAPID details, call this method + /// before sendNotifications() or pass in the details and options to + /// sendNotification. + /// + /// This must be either a URL or a 'mailto:' address + /// The public VAPID key as a base64 encoded string + /// The private VAPID key as a base64 encoded string + public void SetVapidDetails(string subject, string publicKey, string privateKey) + { + SetVapidDetails(new VapidDetails(subject, publicKey, privateKey)); + } - /// - /// When marking requests where you want to define VAPID details, call this method - /// before sendNotifications() or pass in the details and options to - /// sendNotification. - /// - /// - public void SetVapidDetails(VapidDetails vapidDetails) - { - VapidHelper.ValidateSubject(vapidDetails.Subject); - VapidHelper.ValidatePublicKey(vapidDetails.PublicKey); - VapidHelper.ValidatePrivateKey(vapidDetails.PrivateKey); + /// + /// To get a request without sending a push notification call this method. + /// This method will throw an ArgumentException if there is an issue with the input. + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for vapid keys can be passed in if they are unique for each + /// notification. + /// + /// A HttpRequestMessage object that can be sent. + public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? payload, + Dictionary? options = null) + { + var wpo = ConvertOptions(options); + return GenerateRequestDetails(subscription, payload, wpo); + } - _vapidDetails = vapidDetails; + /// + /// To get a request without sending a push notification call this method. + /// This method will throw an ArgumentException if there is an issue with the input. + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for the vapid keys can be passed in if they are unique for each + /// notification. + /// + /// A HttpRequestMessage object that can be sent. + public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? payload, WebPushOptions? options = null) + { + if (!Uri.IsWellFormedUriString(subscription.Endpoint, UriKind.Absolute)) + { + throw new ArgumentException(@"You must pass in a subscription with at least a valid endpoint", nameof(subscription)); } - /// - /// When marking requests where you want to define VAPID details, call this method - /// before sendNotifications() or pass in the details and options to - /// sendNotification. - /// - /// This must be either a URL or a 'mailto:' address - /// The public VAPID key as a base64 encoded string - /// The private VAPID key as a base64 encoded string - public void SetVapidDetails(string subject, string publicKey, string privateKey) + var request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint); + + if (!string.IsNullOrEmpty(payload) && (string.IsNullOrEmpty(subscription.Auth) || + string.IsNullOrEmpty(subscription.P256DH))) { - SetVapidDetails(new VapidDetails(subject, publicKey, privateKey)); + throw new ArgumentException( + @"To send a message with a payload, the subscription must have 'auth' and 'p256dh' keys.", nameof(subscription)); } - /// - /// To get a request without sending a push notification call this method. - /// This method will throw an ArgumentException if there is an issue with the input. - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// - /// Options for the GCM API key and vapid keys can be passed in if they are unique for each - /// notification. - /// - /// A HttpRequestMessage object that can be sent. - public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string payload, - Dictionary options = null) + if (options is not null) { - if (!Uri.IsWellFormedUriString(subscription.Endpoint, UriKind.Absolute)) - { - throw new ArgumentException(@"You must pass in a subscription with at least a valid endpoint"); - } - - var request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint); - - if (!string.IsNullOrEmpty(payload) && (string.IsNullOrEmpty(subscription.Auth) || - string.IsNullOrEmpty(subscription.P256DH))) - { - throw new ArgumentException( - @"To send a message with a payload, the subscription must have 'auth' and 'p256dh' keys."); - } - - var currentGcmApiKey = _gcmApiKey; - var currentVapidDetails = _vapidDetails; - var timeToLive = DefaultTtl; - var extraHeaders = new Dictionary(); - - if (options != null) + if (options.Topic is not null) { - var validOptionsKeys = new List { "headers", "gcmAPIKey", "vapidDetails", "TTL" }; - foreach (var key in options.Keys) - { - if (!validOptionsKeys.Contains(key)) - { - throw new ArgumentException(key + " is an invalid options. The valid options are" + - string.Join(",", validOptionsKeys)); - } - } - - if (options.ContainsKey("headers")) - { - var headers = options["headers"] as Dictionary; - - extraHeaders = headers ?? throw new ArgumentException("options.headers must be of type Dictionary"); - } - - if (options.ContainsKey("gcmAPIKey")) + if (string.IsNullOrWhiteSpace(options.Topic) || options.Topic.Length > 32) { - var gcmApiKey = options["gcmAPIKey"] as string; - - currentGcmApiKey = gcmApiKey ?? throw new ArgumentException("options.gcmAPIKey must be of type string"); + throw new ArgumentException("options.topic must be of type string and not empty and use a maximum of 32 characters from the URL or filename-safe Base64 characters set", nameof(options)); } - - if (options.ContainsKey("vapidDetails")) - { - var vapidDetails = options["vapidDetails"] as VapidDetails; - currentVapidDetails = vapidDetails ?? throw new ArgumentException("options.vapidDetails must be of type VapidDetails"); - } - - if (options.ContainsKey("TTL")) + if (!RegexVariableName().IsMatch(options.Topic)) { - var ttl = options["TTL"] as int?; - if (ttl == null) - { - throw new ArgumentException("options.TTL must be of type int"); - } - - //at this stage ttl cannot be null. - timeToLive = (int)ttl; + throw new ArgumentException("options.topic uses unsupported characters set, use the URL or filename-safe Base64 characters set", nameof(options)); } } + } - string cryptoKeyHeader = null; - request.Headers.Add("TTL", timeToLive.ToString()); + string? cryptoKeyHeader = null; + request.Headers.Add("TTL", (options?.TTL ?? WebPushOptions.DefaultTtl).ToString(CultureInfo.InvariantCulture)); + if (options?.Topic is not null) + { + request.Headers.Add("Topic", options.Topic); + } + if (options?.Urgency is not null) + { + request.Headers.Add("Urgency", options.Urgency.Value.ToKebabCaseLower()); + } - foreach (var header in extraHeaders) + if (options?.ExtraHeaders is not null) + { + foreach (var header in options.ExtraHeaders) { request.Headers.Add(header.Key, header.Value.ToString()); } + } - if (!string.IsNullOrEmpty(payload)) - { - if (string.IsNullOrEmpty(subscription.P256DH) || string.IsNullOrEmpty(subscription.Auth)) - { - throw new ArgumentException( - @"Unable to send a message with payload to this subscription since it doesn't have the required encryption key"); - } - - var encryptedPayload = EncryptPayload(subscription, payload); - - request.Content = new ByteArrayContent(encryptedPayload.Payload); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - request.Content.Headers.ContentLength = encryptedPayload.Payload.Length; - request.Content.Headers.ContentEncoding.Add("aesgcm"); - request.Headers.Add("Encryption", "salt=" + encryptedPayload.Base64EncodeSalt()); - cryptoKeyHeader = @"dh=" + encryptedPayload.Base64EncodePublicKey(); - } - else + var contentEncoding = options?.ContentEncoding ?? WebPushOptions.DefaultContentEncoding; + if (!string.IsNullOrEmpty(payload)) + { + if (string.IsNullOrEmpty(subscription.P256DH) || string.IsNullOrEmpty(subscription.Auth)) { - request.Content = new ByteArrayContent(new byte[0]); - request.Content.Headers.ContentLength = 0; + throw new ArgumentException( + @"Unable to send a message with payload to this subscription since it doesn't have the required encryption key", nameof(subscription)); } - var isGcm = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send"); - var isFcm = subscription.Endpoint.StartsWith(@"https://fcm.googleapis.com/fcm/send/"); + var encryptedPayload = EncryptPayload(subscription, payload); - if (isGcm) + request.Content = new ByteArrayContent(encryptedPayload.Payload); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + request.Content.Headers.ContentLength = encryptedPayload.Payload.Length; + request.Content.Headers.ContentEncoding.Add(contentEncoding.ToKebabCaseLower()); + if (contentEncoding == ContentEncoding.Aesgcm) { - if (!string.IsNullOrEmpty(currentGcmApiKey)) - { - request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); - } + request.Headers.Add("Encryption", "salt=" + encryptedPayload.Base64EncodeSalt()); } - else if (currentVapidDetails != null) - { - var uri = new Uri(subscription.Endpoint); - var audience = uri.Scheme + @"://" + uri.Host; + cryptoKeyHeader = @"dh=" + encryptedPayload.Base64EncodePublicKey(); + } + else + { + request.Content = new ByteArrayContent([]); + request.Content.Headers.ContentLength = 0; + } - var vapidHeaders = VapidHelper.GetVapidHeaders(audience, currentVapidDetails.Subject, - currentVapidDetails.PublicKey, currentVapidDetails.PrivateKey, currentVapidDetails.Expiration); - request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); + var vapidDetails = options?.VapidDetails ?? _vapidDetails; + if (vapidDetails is not null) + { + var uri = new Uri(subscription.Endpoint); + var audience = uri.Scheme + @"://" + uri.Host; + var vapidHeaders = VapidHelper.GetVapidHeaders(audience, vapidDetails.Subject, vapidDetails.PublicKey, vapidDetails.PrivateKey, vapidDetails.Expiration, contentEncoding); + request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); + if (contentEncoding == ContentEncoding.Aesgcm) + { if (string.IsNullOrEmpty(cryptoKeyHeader)) { cryptoKeyHeader = vapidHeaders["Crypto-Key"]; @@ -245,179 +211,212 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"]; } } - else if (isFcm && !string.IsNullOrEmpty(currentGcmApiKey)) - { - request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); - } - + } + if (contentEncoding == ContentEncoding.Aesgcm) + { request.Headers.Add("Crypto-Key", cryptoKeyHeader); - return request; } + return request; + } - private static EncryptionResult EncryptPayload(PushSubscription subscription, string payload) + private static EncryptionResult EncryptPayload(PushSubscription subscription, string payload) + { + try + { + return Encryptor.Encrypt(subscription.P256DH, subscription.Auth, payload); + } + catch (Exception ex) { - try + if (ex is FormatException || ex is ArgumentException || ex is CryptographicException) { - return Encryptor.Encrypt(subscription.P256DH, subscription.Auth, payload); + throw new InvalidEncryptionDetailsException("Unable to encrypt the payload with the encryption key of this subscription.", subscription); } - catch (Exception ex) - { - if (ex is FormatException || ex is ArgumentException) - { - throw new InvalidEncryptionDetailsException("Unable to encrypt the payload with the encryption key of this subscription.", subscription); - } - throw; - } + throw; } + } - /// - /// To send a push notification call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// - /// Options for the GCM API key and vapid keys can be passed in if they are unique for each - /// notification. - /// - public void SendNotification(PushSubscription subscription, string payload = null, - Dictionary options = null) - { - SendNotificationAsync(subscription, payload, options).ConfigureAwait(false).GetAwaiter().GetResult(); - } + /// + /// To send a push notification call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for the web push request, including vapid keys, if they are unique for each + /// notification. + /// + public void SendNotification(PushSubscription subscription, string? payload = null, WebPushOptions? options = null) + { + SendNotificationAsync(subscription, payload, options).ConfigureAwait(false).GetAwaiter().GetResult(); + } - /// - /// To send a push notification call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// The vapid details for the notification. - public void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails) - { - var options = new Dictionary { ["vapidDetails"] = vapidDetails }; - SendNotification(subscription, payload, options); - } + /// + /// To send a push notification call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for vapid keys can be passed in if they are unique for each + /// notification. + /// + public void SendNotification(PushSubscription subscription, string? payload = null, + Dictionary? options = null) + { + SendNotificationAsync(subscription, payload, options).ConfigureAwait(false).GetAwaiter().GetResult(); + } - /// - /// To send a push notification call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// The GCM API key - public void SendNotification(PushSubscription subscription, string payload, string gcmApiKey) - { - var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey }; - SendNotification(subscription, payload, options); - } + /// + /// To send a push notification call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// The vapid details for the notification. + public void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails) + { + var options = new WebPushOptions { VapidDetails = vapidDetails, }; + SendNotification(subscription, payload, options); + } + /// + /// To send a push notification asynchronous call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for the web push request, including vapid keys, if they are unique for each + /// notification. + /// + /// The cancellation token to cancel operation. + public async Task SendNotificationAsync(PushSubscription subscription, string? payload = null, WebPushOptions? options = null, CancellationToken cancellationToken = default) + { + var request = GenerateRequestDetails(subscription, payload, options); + var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); - /// - /// To send a push notification asynchronous call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// - /// Options for the GCM API key and vapid keys can be passed in if they are unique for each - /// notification. - /// - /// The cancellation token to cancel operation. - public async Task SendNotificationAsync(PushSubscription subscription, string payload = null, - Dictionary options = null, CancellationToken cancellationToken = default) - { - var request = GenerateRequestDetails(subscription, payload, options); - var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + await HandleResponse(response, subscription).ConfigureAwait(false); + } - await HandleResponse(response, subscription).ConfigureAwait(false); - } + /// + /// To send a push notification asynchronous call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// + /// Options for vapid keys can be passed in if they are unique for each + /// notification. + /// + /// The cancellation token to cancel operation. + public async Task SendNotificationAsync(PushSubscription subscription, string? payload = null, + Dictionary? options = null, CancellationToken cancellationToken = default) + { + var request = GenerateRequestDetails(subscription, payload, options); + var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + await HandleResponse(response, subscription).ConfigureAwait(false); + } - /// - /// To send a push notification asynchronous call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// The vapid details for the notification. - /// - public async Task SendNotificationAsync(PushSubscription subscription, string payload, - VapidDetails vapidDetails, CancellationToken cancellationToken = default) + /// + /// To send a push notification asynchronous call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// The vapid details for the notification. + /// + public async Task SendNotificationAsync(PushSubscription subscription, string payload, + VapidDetails vapidDetails, CancellationToken cancellationToken = default) + { + var options = new WebPushOptions { VapidDetails = vapidDetails, }; + await SendNotificationAsync(subscription, payload, options, cancellationToken).ConfigureAwait(false); + } + + /// + /// Handle Web Push responses. + /// + /// + /// + private static async Task HandleResponse(HttpResponseMessage response, PushSubscription subscription) + { + // Successful + if (response.IsSuccessStatusCode) { - var options = new Dictionary { ["vapidDetails"] = vapidDetails }; - await SendNotificationAsync(subscription, payload, options, cancellationToken).ConfigureAwait(false); + return; } - /// - /// To send a push notification asynchronous call this method with a subscription, optional payload and any options - /// Will exception if unsuccessful - /// - /// The PushSubscription you wish to send the notification to. - /// The payload you wish to send to the user - /// The GCM API key - /// - public async Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey, CancellationToken cancellationToken = default) + // Error + var responseCodeMessage = $"Received unexpected response code: {(int)response.StatusCode}"; + switch (response.StatusCode) { - var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey }; - await SendNotificationAsync(subscription, payload, options, cancellationToken).ConfigureAwait(false); + case HttpStatusCode.BadRequest: + responseCodeMessage = "Bad Request"; + break; + + case HttpStatusCode.RequestEntityTooLarge: + responseCodeMessage = "Payload too large"; + break; + + case (HttpStatusCode)429: + responseCodeMessage = "Too many request"; + break; + + case HttpStatusCode.NotFound: + case HttpStatusCode.Gone: + responseCodeMessage = "Subscription no longer valid"; + break; } - /// - /// Handle Web Push responses. - /// - /// - /// - private static async Task HandleResponse(HttpResponseMessage response, PushSubscription subscription) + string? details = null; + if (response.Content != null) { - // Successful - if (response.IsSuccessStatusCode) - { - return; - } + details = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } - // Error - var responseCodeMessage = @"Received unexpected response code: " + (int)response.StatusCode; - switch (response.StatusCode) - { - case HttpStatusCode.BadRequest: - responseCodeMessage = "Bad Request"; - break; + var message = string.IsNullOrEmpty(details) + ? responseCodeMessage + : $"{responseCodeMessage}. Details: {details}"; - case HttpStatusCode.RequestEntityTooLarge: - responseCodeMessage = "Payload too large"; - break; + throw new WebPushException(message, subscription, response); + } - case (HttpStatusCode)429: - responseCodeMessage = "Too many request"; + private static WebPushOptions ConvertOptions(Dictionary? options = null) + { + var wpo = new WebPushOptions(); + foreach (var option in options ?? []) + { + switch (option.Key) + { + case "TTL": + var ttl = option.Value as int? ?? throw new ArgumentException("options.TTL must be of type int"); + wpo.TTL = ttl; break; - - case HttpStatusCode.NotFound: - case HttpStatusCode.Gone: - responseCodeMessage = "Subscription no longer valid"; + case "vapidDetails": + var vapids = option.Value as VapidDetails ?? throw new ArgumentException("options.vapidDetails must be of type VapidDetails"); + wpo.VapidDetails = vapids; break; + case "headers": + var headers = option.Value as Dictionary ?? throw new ArgumentException("options.headers must be of type Dictionary"); + wpo.ExtraHeaders = headers; + break; + default: + throw new ArgumentException($"{option.Key} is an invalid options"); } - - string details = null; - if (response.Content != null) - { - details = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - } - - var message = string.IsNullOrEmpty(details) - ? responseCodeMessage - : $"{responseCodeMessage}. Details: {details}"; - - throw new WebPushException(message, subscription, response); } + return wpo; + } - public void Dispose() + public void Dispose() + { + if (_httpClient != null && _isHttpClientInternallyCreated) { - if (_httpClient != null && _isHttpClientInternallyCreated) - { - _httpClient.Dispose(); - _httpClient = null; - } + _httpClient.Dispose(); + _httpClient = null; } } + + [GeneratedRegex(@"^[A-Za-z0-9\-_]+$", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex RegexVariableName(); } \ No newline at end of file diff --git a/WebPush/packages.lock.json b/WebPush/packages.lock.json index 44fe018..4c08e8e 100644 --- a/WebPush/packages.lock.json +++ b/WebPush/packages.lock.json @@ -1,55 +1,41 @@ { "version": 1, "dependencies": { - ".NETFramework,Version=v4.8": { - "Microsoft.NETFramework.ReferenceAssemblies": { + "net10.0": { + "Microsoft.IdentityModel.JsonWebTokens": { "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, - "Microsoft.SourceLink.GitHub": { + "Microsoft.IdentityModel.Tokens": { "type": "Direct", - "requested": "[10.0.203, )", - "resolved": "10.0.203", - "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { - "Microsoft.Build.Tasks.Git": "10.0.203", - "Microsoft.SourceLink.Common": "10.0.203", - "System.IO.Hashing": "10.0.7" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.18.0" } }, - "Portable.BouncyCastle": { - "type": "Direct", - "requested": "[1.9.0, )", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, - "System.Text.Json": { + "Microsoft.NET.ILLink.Tasks": { "type": "Direct", "requested": "[10.0.7, )", "resolved": "10.0.7", - "contentHash": "F8Pu2QLUMeniVbtiyk7n7LCfFYxlcJ8ASaSwglJyq6dxa34iCQrikQszsgJClIJWuSWjcyhKkV7daAzYJqeVwA==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.7", - "System.Buffers": "4.6.1", - "System.IO.Pipelines": "10.0.7", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2", - "System.Text.Encodings.Web": "10.0.7", - "System.Threading.Tasks.Extensions": "4.6.3", - "System.ValueTuple": "4.6.2" - } + "contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw==" }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "g0Xp9A+B8jCf5pNIIhFOQXPJkte3D87shfTLY+ylwfSh22U5oQH6tvvmcUuqJvt/wtwKk0WdNp2OGEczHJlJdg==", + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[10.0.203, )", + "resolved": "10.0.203", + "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==", "dependencies": { - "System.Threading.Tasks.Extensions": "4.6.3" + "Microsoft.Build.Tasks.Git": "10.0.203", + "Microsoft.SourceLink.Common": "10.0.203", + "System.IO.Hashing": "10.0.7" } }, "Microsoft.Build.Tasks.Git": { @@ -60,85 +46,69 @@ "System.IO.Hashing": "10.0.7" } }, - "Microsoft.NETFramework.ReferenceAssemblies.net48": { + "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==" + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, - "Microsoft.SourceLink.Common": { + "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "10.0.203", - "contentHash": "QYAnhBCOkT3ZUT/fHag11+bamwlbZ3U9Vi/WfKrD9emdUf1t3aqjWv0V2KtEGHSRSC81aBc8Oy/mvyGpEYd9Pg==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" - }, - "System.IO.Hashing": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ==", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, - "System.IO.Pipelines": { + "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "LTxXYYKmRhPKWveYmfzuRTUnzsfY7CN+WOq6aTRgYE9vJ8BUvIWPCaSx4HxqBwXViTPSjR9cHDOVuVPuZGRR/Q==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Threading.Tasks.Extensions": "4.6.3" - } + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, - "System.Memory": { + "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "System.Buffers": "4.6.1", - "System.Numerics.Vectors": "4.6.1", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" - }, - "System.Runtime.CompilerServices.Unsafe": { + "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "6.1.2", - "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" + "resolved": "10.0.203", + "contentHash": "QYAnhBCOkT3ZUT/fHag11+bamwlbZ3U9Vi/WfKrD9emdUf1t3aqjWv0V2KtEGHSRSC81aBc8Oy/mvyGpEYd9Pg==" }, - "System.Text.Encodings.Web": { + "System.IO.Hashing": { "type": "Transitive", "resolved": "10.0.7", - "contentHash": "WUH+viO8VDG8NpFKvOBwpeyKUiPOMz3kQpA6AKCD4b2NG1pBhyC4AwTb357iZmTxZDnkM4IsFnvzN8W8OKmsHg==", + "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ==" + } + }, + "net8.0": { + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Direct", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "Microsoft.IdentityModel.Tokens": { + "type": "Direct", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.18.0" } }, - "System.ValueTuple": { - "type": "Transitive", - "resolved": "4.6.2", - "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" - } - }, - ".NETStandard,Version=v2.1": { + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[8.0.26, )", + "resolved": "8.0.26", + "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" + }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[10.0.203, )", @@ -150,29 +120,6 @@ "System.IO.Hashing": "10.0.7" } }, - "Portable.BouncyCastle": { - "type": "Direct", - "requested": "[1.9.0, )", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, - "System.Text.Json": { - "type": "Direct", - "requested": "[10.0.7, )", - "resolved": "10.0.7", - "contentHash": "F8Pu2QLUMeniVbtiyk7n7LCfFYxlcJ8ASaSwglJyq6dxa34iCQrikQszsgJClIJWuSWjcyhKkV7daAzYJqeVwA==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.7", - "System.IO.Pipelines": "10.0.7", - "System.Runtime.CompilerServices.Unsafe": "6.1.2", - "System.Text.Encodings.Web": "10.0.7" - } - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "g0Xp9A+B8jCf5pNIIhFOQXPJkte3D87shfTLY+ylwfSh22U5oQH6tvvmcUuqJvt/wtwKk0WdNp2OGEczHJlJdg==" - }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "10.0.203", @@ -181,59 +128,30 @@ "System.IO.Hashing": "10.0.7" } }, - "Microsoft.SourceLink.Common": { + "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.203", - "contentHash": "QYAnhBCOkT3ZUT/fHag11+bamwlbZ3U9Vi/WfKrD9emdUf1t3aqjWv0V2KtEGHSRSC81aBc8Oy/mvyGpEYd9Pg==" + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, - "System.IO.Hashing": { + "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "LTxXYYKmRhPKWveYmfzuRTUnzsfY7CN+WOq6aTRgYE9vJ8BUvIWPCaSx4HxqBwXViTPSjR9cHDOVuVPuZGRR/Q==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.1.2", - "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "WUH+viO8VDG8NpFKvOBwpeyKUiPOMz3kQpA6AKCD4b2NG1pBhyC4AwTb357iZmTxZDnkM4IsFnvzN8W8OKmsHg==", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - } - }, - "net10.0": { - "Microsoft.SourceLink.GitHub": { - "type": "Direct", - "requested": "[10.0.203, )", - "resolved": "10.0.203", - "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "10.0.203", - "Microsoft.SourceLink.Common": "10.0.203", - "System.IO.Hashing": "10.0.7" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, - "Portable.BouncyCastle": { - "type": "Direct", - "requested": "[1.9.0, )", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, - "Microsoft.Build.Tasks.Git": { + "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "10.0.203", - "contentHash": "m56WtzvIcL6t7JR3c7ogYitHizNM2QnRSo8yqxrQi+m5E/GGyDEmqymP+2p6YsFXn0j/Tzz67s4FQnrTLC7GKQ==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "System.IO.Hashing": "10.0.7" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, "Microsoft.SourceLink.Common": { @@ -247,7 +165,32 @@ "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ==" } }, - "net8.0": { + "net9.0": { + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Direct", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.18.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Direct", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.18.0" + } + }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[9.0.15, )", + "resolved": "9.0.15", + "contentHash": "EejcbfCMR77Dthy77qxRbEShmzLApHZUPqXMBVQK+A0pNrRThkaHoGGMGvbq/gTkC/waKcDEgjBkbaejB58Wtw==" + }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[10.0.203, )", @@ -259,12 +202,6 @@ "System.IO.Hashing": "10.0.7" } }, - "Portable.BouncyCastle": { - "type": "Direct", - "requested": "[1.9.0, )", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "10.0.203", @@ -273,41 +210,30 @@ "System.IO.Hashing": "10.0.7" } }, - "Microsoft.SourceLink.Common": { + "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.203", - "contentHash": "QYAnhBCOkT3ZUT/fHag11+bamwlbZ3U9Vi/WfKrD9emdUf1t3aqjWv0V2KtEGHSRSC81aBc8Oy/mvyGpEYd9Pg==" + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, - "System.IO.Hashing": { + "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ==" - } - }, - "net9.0": { - "Microsoft.SourceLink.GitHub": { - "type": "Direct", - "requested": "[10.0.203, )", - "resolved": "10.0.203", - "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { - "Microsoft.Build.Tasks.Git": "10.0.203", - "Microsoft.SourceLink.Common": "10.0.203", - "System.IO.Hashing": "10.0.7" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, - "Portable.BouncyCastle": { - "type": "Direct", - "requested": "[1.9.0, )", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, - "Microsoft.Build.Tasks.Git": { + "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "10.0.203", - "contentHash": "m56WtzvIcL6t7JR3c7ogYitHizNM2QnRSo8yqxrQi+m5E/GGyDEmqymP+2p6YsFXn0j/Tzz67s4FQnrTLC7GKQ==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "System.IO.Hashing": "10.0.7" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, "Microsoft.SourceLink.Common": {