From 02bdefab55297e5c41af2e8744abfbd64ce6ba95 Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:14:58 +0200 Subject: [PATCH 01/10] Switch to artifacts output --- .gitignore | 1 + Directory.Build.props | 1 + 2 files changed, 2 insertions(+) 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 From eeeabb48dbe35c20e53d0ce320bd6c6ae370fa55 Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:42:27 +0200 Subject: [PATCH 02/10] Introduce net base cryptography functions --- WebPush.Test/packages.lock.json | 240 +++++++++----- WebPush/Model/ContentEncoding.cs | 7 + WebPush/Model/EncryptionResult.cs | 35 +- .../InvalidEncryptionDetailsException.cs | 17 +- WebPush/Model/PushSubscription.cs | 29 +- WebPush/Model/VapidDetails.cs | 42 ++- WebPush/Model/WebPushException.cs | 25 +- WebPush/Model/WebPushOptions.cs | 19 ++ WebPush/Urgency.cs | 9 + WebPush/Util/ECKeyHelper.cs | 112 ++++--- WebPush/Util/Encryptor.cs | 201 +++++------ WebPush/Util/EnumHelper.cs | 18 + WebPush/Util/JwsSigner.cs | 79 ----- WebPush/Util/UrlBase64.cs | 34 -- WebPush/VapidHelper.cs | 229 ++++++------- WebPush/WebPush.csproj | 17 +- WebPush/packages.lock.json | 312 +++++++----------- 17 files changed, 641 insertions(+), 784 deletions(-) create mode 100644 WebPush/Model/ContentEncoding.cs create mode 100644 WebPush/Model/WebPushOptions.cs create mode 100644 WebPush/Urgency.cs create mode 100644 WebPush/Util/EnumHelper.cs delete mode 100644 WebPush/Util/JwsSigner.cs delete mode 100644 WebPush/Util/UrlBase64.cs diff --git a/WebPush.Test/packages.lock.json b/WebPush.Test/packages.lock.json index e00192a..ead2608 100644 --- a/WebPush.Test/packages.lock.json +++ b/WebPush.Test/packages.lock.json @@ -70,14 +70,6 @@ "System.Diagnostics.DiagnosticSource": "5.0.0" } }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "10.0.7", - "contentHash": "g0Xp9A+B8jCf5pNIIhFOQXPJkte3D87shfTLY+ylwfSh22U5oQH6tvvmcUuqJvt/wtwKk0WdNp2OGEczHJlJdg==", - "dependencies": { - "System.Threading.Tasks.Extensions": "4.6.3" - } - }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.4.0", @@ -143,15 +135,10 @@ "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==" + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" }, "System.Collections.Immutable": { "type": "Transitive", @@ -171,30 +158,20 @@ "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" - } - }, "System.Memory": { "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", "dependencies": { - "System.Buffers": "4.6.1", - "System.Numerics.Vectors": "4.6.1", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" } }, "System.Numerics.Vectors": { "type": "Transitive", - "resolved": "4.6.1", - "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" }, "System.Reflection.Metadata": { "type": "Transitive", @@ -207,53 +184,19 @@ }, "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==", - "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" - } - }, - "System.Text.Json": { - "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": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" }, "System.Threading.Tasks.Extensions": { "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "System.Runtime.CompilerServices.Unsafe": "4.5.3" } }, - "System.ValueTuple": { - "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, )" - } + "type": "Project" } }, "net10.0": { @@ -320,6 +263,49 @@ "resolved": "18.4.0", "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" }, + "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.17.0", + "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.17.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.17.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.17.0" + } + }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "2.2.1", @@ -385,11 +371,6 @@ "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,7 +379,8 @@ "webpush": { "type": "Project", "dependencies": { - "Portable.BouncyCastle": "[1.9.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.17.0, )", + "Microsoft.IdentityModel.Tokens": "[8.17.0, )" } } }, @@ -466,6 +448,49 @@ "resolved": "18.4.0", "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" }, + "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.17.0", + "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.17.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.17.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.17.0" + } + }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "2.2.1", @@ -531,11 +556,6 @@ "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,7 +564,8 @@ "webpush": { "type": "Project", "dependencies": { - "Portable.BouncyCastle": "[1.9.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.17.0, )", + "Microsoft.IdentityModel.Tokens": "[8.17.0, )" } } }, @@ -612,6 +633,49 @@ "resolved": "18.4.0", "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" }, + "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.17.0", + "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.17.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.17.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.17.0" + } + }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "2.2.1", @@ -677,11 +741,6 @@ "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 +749,8 @@ "webpush": { "type": "Project", "dependencies": { - "Portable.BouncyCastle": "[1.9.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.17.0, )", + "Microsoft.IdentityModel.Tokens": "[8.17.0, )" } } } 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..213ca97 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/packages.lock.json b/WebPush/packages.lock.json index 44fe018..ba63072 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.17.0, )", + "resolved": "8.17.0", + "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" + "Microsoft.IdentityModel.Tokens": "8.17.0" } }, - "Microsoft.SourceLink.GitHub": { + "Microsoft.IdentityModel.Tokens": { "type": "Direct", - "requested": "[10.0.203, )", - "resolved": "10.0.203", - "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==", + "requested": "[8.17.0, )", + "resolved": "8.17.0", + "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", "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.17.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.17.0", + "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" }, - "System.Memory": { + "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "resolved": "8.17.0", + "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", "dependencies": { - "System.Buffers": "4.6.1", - "System.Numerics.Vectors": "4.6.1", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "Microsoft.IdentityModel.Abstractions": "8.17.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.17.0, )", + "resolved": "8.17.0", + "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", "dependencies": { - "System.Buffers": "4.6.1", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "Microsoft.IdentityModel.Tokens": "8.17.0" } }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.6.3", - "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "Microsoft.IdentityModel.Tokens": { + "type": "Direct", + "requested": "[8.17.0, )", + "resolved": "8.17.0", + "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.1.2" + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.17.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.17.0", + "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" }, - "Microsoft.Build.Tasks.Git": { + "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "10.0.203", - "contentHash": "m56WtzvIcL6t7JR3c7ogYitHizNM2QnRSo8yqxrQi+m5E/GGyDEmqymP+2p6YsFXn0j/Tzz67s4FQnrTLC7GKQ==", + "resolved": "8.17.0", + "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", "dependencies": { - "System.IO.Hashing": "10.0.7" + "Microsoft.IdentityModel.Abstractions": "8.17.0" } }, "Microsoft.SourceLink.Common": { @@ -247,7 +165,32 @@ "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ==" } }, - "net8.0": { + "net9.0": { + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Direct", + "requested": "[8.17.0, )", + "resolved": "8.17.0", + "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.17.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Direct", + "requested": "[8.17.0, )", + "resolved": "8.17.0", + "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.17.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.17.0", + "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" }, - "Microsoft.Build.Tasks.Git": { + "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "10.0.203", - "contentHash": "m56WtzvIcL6t7JR3c7ogYitHizNM2QnRSo8yqxrQi+m5E/GGyDEmqymP+2p6YsFXn0j/Tzz67s4FQnrTLC7GKQ==", + "resolved": "8.17.0", + "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", "dependencies": { - "System.IO.Hashing": "10.0.7" + "Microsoft.IdentityModel.Abstractions": "8.17.0" } }, "Microsoft.SourceLink.Common": { From fa636b1778ba1c951667796721387dc45070b270 Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:51:39 +0200 Subject: [PATCH 03/10] Minimal adaptions for IWebPushClient --- WebPush/IWebPushClient.cs | 199 ++++++------ WebPush/WebPushClient.cs | 651 +++++++++++++++++++------------------- 2 files changed, 424 insertions(+), 426 deletions(-) diff --git a/WebPush/IWebPushClient.cs b/WebPush/IWebPushClient.cs index 005be35..665da30 100644 --- a/WebPush/IWebPushClient.cs +++ b/WebPush/IWebPushClient.cs @@ -4,114 +4,113 @@ 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 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 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 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 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 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 - /// 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 + /// 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 - /// 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 GCM API key + void SendNotification(PushSubscription subscription, string payload, string gcmApiKey); - /// - /// 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 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 - /// 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 + /// 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 - /// 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 GCM API key + /// + Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs index df1bfe1..7551fee 100644 --- a/WebPush/WebPushClient.cs +++ b/WebPush/WebPushClient.cs @@ -11,413 +11,412 @@ [assembly: InternalsVisibleTo("WebPush.Test")] -namespace WebPush +namespace WebPush; + +public class WebPushClient : IWebPushClient { - public class WebPushClient : IWebPushClient - { - // default TTL is 4 weeks. - private const int DefaultTtl = 2419200; - private readonly HttpClientHandler _httpClientHandler; + // default TTL is 4 weeks. + private const int DefaultTtl = 2419200; + private readonly HttpClientHandler? _httpClientHandler; - private string _gcmApiKey; - private HttpClient _httpClient; - private VapidDetails _vapidDetails; + private string? _gcmApiKey; + 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; } - } - /// - /// 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; - } + _isHttpClientInternallyCreated = true; + _httpClient = _httpClientHandler == null + ? new HttpClient() + : new HttpClient(_httpClientHandler); - if (string.IsNullOrEmpty(gcmApiKey)) - { - throw new ArgumentException(@"The GCM API Key should be a non-empty string or null."); - } - - _gcmApiKey = gcmApiKey; + return _httpClient; } + } - /// - /// 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) + /// + /// 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) { - VapidHelper.ValidateSubject(vapidDetails.Subject); - VapidHelper.ValidatePublicKey(vapidDetails.PublicKey); - VapidHelper.ValidatePrivateKey(vapidDetails.PrivateKey); - - _vapidDetails = vapidDetails; + _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. - /// - /// 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) + if (string.IsNullOrEmpty(gcmApiKey)) { - SetVapidDetails(new VapidDetails(subject, publicKey, privateKey)); + throw new ArgumentException(@"The GCM API Key should be a non-empty string or 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. - public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string payload, - Dictionary options = 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."); - } + _gcmApiKey = gcmApiKey; + } - var currentGcmApiKey = _gcmApiKey; - var currentVapidDetails = _vapidDetails; - var timeToLive = DefaultTtl; - var extraHeaders = new Dictionary(); + /// + /// 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 (options != 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)); - } - } + _vapidDetails = vapidDetails; + } - if (options.ContainsKey("headers")) - { - var headers = options["headers"] as Dictionary; + /// + /// 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)); + } - extraHeaders = headers ?? throw new ArgumentException("options.headers must be of type Dictionary"); - } + /// + /// 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 (!Uri.IsWellFormedUriString(subscription.Endpoint, UriKind.Absolute)) + { + throw new ArgumentException(@"You must pass in a subscription with at least a valid endpoint"); + } - if (options.ContainsKey("gcmAPIKey")) - { - var gcmApiKey = options["gcmAPIKey"] as string; + var request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint); - currentGcmApiKey = gcmApiKey ?? throw new ArgumentException("options.gcmAPIKey must be of type string"); - } + 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."); + } - if (options.ContainsKey("vapidDetails")) - { - var vapidDetails = options["vapidDetails"] as VapidDetails; - currentVapidDetails = vapidDetails ?? throw new ArgumentException("options.vapidDetails must be of type VapidDetails"); - } + var currentGcmApiKey = _gcmApiKey; + var currentVapidDetails = _vapidDetails; + var timeToLive = DefaultTtl; + var extraHeaders = new Dictionary(); - if (options.ContainsKey("TTL")) + if (options != null) + { + var validOptionsKeys = new List { "headers", "gcmAPIKey", "vapidDetails", "TTL" }; + foreach (var key in options.Keys) + { + if (!validOptionsKeys.Contains(key)) { - 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(key + " is an invalid options. The valid options are" + + string.Join(",", validOptionsKeys)); } } - string cryptoKeyHeader = null; - request.Headers.Add("TTL", timeToLive.ToString()); - - foreach (var header in extraHeaders) + if (options.ContainsKey("headers")) { - request.Headers.Add(header.Key, header.Value.ToString()); + var headers = options["headers"] as Dictionary; + + extraHeaders = headers ?? throw new ArgumentException("options.headers must be of type Dictionary"); } - if (!string.IsNullOrEmpty(payload)) + if (options.ContainsKey("gcmAPIKey")) { - 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); + var gcmApiKey = options["gcmAPIKey"] as string; - 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(); + currentGcmApiKey = gcmApiKey ?? throw new ArgumentException("options.gcmAPIKey must be of type string"); } - else + + if (options.ContainsKey("vapidDetails")) { - request.Content = new ByteArrayContent(new byte[0]); - request.Content.Headers.ContentLength = 0; + var vapidDetails = options["vapidDetails"] as VapidDetails; + currentVapidDetails = vapidDetails ?? throw new ArgumentException("options.vapidDetails must be of type VapidDetails"); } - var isGcm = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send"); - var isFcm = subscription.Endpoint.StartsWith(@"https://fcm.googleapis.com/fcm/send/"); - - if (isGcm) + if (options.ContainsKey("TTL")) { - if (!string.IsNullOrEmpty(currentGcmApiKey)) + var ttl = options["TTL"] as int?; + if (ttl == null) { - request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); + throw new ArgumentException("options.TTL must be of type int"); } - } - else if (currentVapidDetails != null) - { - var uri = new Uri(subscription.Endpoint); - var audience = uri.Scheme + @"://" + uri.Host; - var vapidHeaders = VapidHelper.GetVapidHeaders(audience, currentVapidDetails.Subject, - currentVapidDetails.PublicKey, currentVapidDetails.PrivateKey, currentVapidDetails.Expiration); - request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); - if (string.IsNullOrEmpty(cryptoKeyHeader)) - { - cryptoKeyHeader = vapidHeaders["Crypto-Key"]; - } - else - { - cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"]; - } - } - else if (isFcm && !string.IsNullOrEmpty(currentGcmApiKey)) - { - request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); + //at this stage ttl cannot be null. + timeToLive = (int)ttl; } + } - request.Headers.Add("Crypto-Key", cryptoKeyHeader); - return request; + string? cryptoKeyHeader = null; + request.Headers.Add("TTL", timeToLive.ToString()); + + foreach (var header in extraHeaders) + { + request.Headers.Add(header.Key, header.Value.ToString()); } - private static EncryptionResult EncryptPayload(PushSubscription subscription, string payload) + if (!string.IsNullOrEmpty(payload)) { - try + if (string.IsNullOrEmpty(subscription.P256DH) || string.IsNullOrEmpty(subscription.Auth)) { - return Encryptor.Encrypt(subscription.P256DH, subscription.Auth, payload); + throw new ArgumentException( + @"Unable to send a message with payload to this subscription since it doesn't have the required encryption key"); } - 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; - } - } + var encryptedPayload = EncryptPayload(subscription, payload); - /// - /// 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) + 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 { - SendNotificationAsync(subscription, payload, options).ConfigureAwait(false).GetAwaiter().GetResult(); + request.Content = new ByteArrayContent(new byte[0]); + request.Content.Headers.ContentLength = 0; } - /// - /// 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 isGcm = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send"); + var isFcm = subscription.Endpoint.StartsWith(@"https://fcm.googleapis.com/fcm/send/"); + + if (isGcm) { - var options = new Dictionary { ["vapidDetails"] = vapidDetails }; - SendNotification(subscription, payload, options); + if (!string.IsNullOrEmpty(currentGcmApiKey)) + { + request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); + } } + else if (currentVapidDetails != null) + { + var uri = new Uri(subscription.Endpoint); + var audience = uri.Scheme + @"://" + uri.Host; - /// - /// 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 vapidHeaders = VapidHelper.GetVapidHeaders(audience, currentVapidDetails.Subject, + currentVapidDetails.PublicKey, currentVapidDetails.PrivateKey, currentVapidDetails.Expiration); + request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); + if (string.IsNullOrEmpty(cryptoKeyHeader)) + { + cryptoKeyHeader = vapidHeaders["Crypto-Key"]; + } + else + { + cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"]; + } + } + else if (isFcm && !string.IsNullOrEmpty(currentGcmApiKey)) { - var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey }; - SendNotification(subscription, payload, options); + request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); } + request.Headers.Add("Crypto-Key", cryptoKeyHeader); + return request; + } - /// - /// 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) + private static EncryptionResult EncryptPayload(PushSubscription subscription, string payload) + { + try + { + return Encryptor.Encrypt(subscription.P256DH, subscription.Auth, payload); + } + catch (Exception ex) { - var request = GenerateRequestDetails(subscription, payload, options); - var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + if (ex is FormatException || ex is ArgumentException) + { + throw new InvalidEncryptionDetailsException("Unable to encrypt the payload with the encryption key of this subscription.", subscription); + } - await HandleResponse(response, subscription).ConfigureAwait(false); + throw; } + } - /// - /// 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 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 + /// 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 + /// 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 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); + } + + /// + /// 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 Dictionary { ["vapidDetails"] = vapidDetails }; + await SendNotificationAsync(subscription, payload, options, 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 + /// The GCM API key + /// + public async Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey, CancellationToken cancellationToken = default) + { + var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey }; + 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; - } - - // Error - var responseCodeMessage = @"Received unexpected response code: " + (int)response.StatusCode; - switch (response.StatusCode) - { - 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; - } - - string details = null; - if (response.Content != null) - { - details = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - } + details = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } - var message = string.IsNullOrEmpty(details) - ? responseCodeMessage - : $"{responseCodeMessage}. Details: {details}"; + var message = string.IsNullOrEmpty(details) + ? responseCodeMessage + : $"{responseCodeMessage}. Details: {details}"; - throw new WebPushException(message, subscription, response); - } + throw new WebPushException(message, subscription, response); + } - public void Dispose() + public void Dispose() + { + if (_httpClient != null && _isHttpClientInternallyCreated) { - if (_httpClient != null && _isHttpClientInternallyCreated) - { - _httpClient.Dispose(); - _httpClient = null; - } + _httpClient.Dispose(); + _httpClient = null; } } } \ No newline at end of file From ac568a2ec5cfdfe43bd2837ff0663d1ee69dd2af Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:18:54 +0200 Subject: [PATCH 04/10] Adapt web push client add new interfaces --- WebPush/IWebPushClient.cs | 38 +++++ WebPush/WebPushClient.cs | 335 +++++++++++++++++++++++++++++--------- 2 files changed, 297 insertions(+), 76 deletions(-) diff --git a/WebPush/IWebPushClient.cs b/WebPush/IWebPushClient.cs index 665da30..1dc2171 100644 --- a/WebPush/IWebPushClient.cs +++ b/WebPush/IWebPushClient.cs @@ -48,6 +48,31 @@ public interface IWebPushClient : IDisposable 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 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 @@ -79,6 +104,19 @@ void SendNotification(PushSubscription subscription, string? payload = null, /// The GCM API key void SendNotification(PushSubscription subscription, string payload, string gcmApiKey); + /// + /// 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 diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs index 7551fee..26e9851 100644 --- a/WebPush/WebPushClient.cs +++ b/WebPush/WebPushClient.cs @@ -1,9 +1,11 @@ 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.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using WebPush.Model; @@ -13,7 +15,7 @@ namespace WebPush; -public class WebPushClient : IWebPushClient +public partial class WebPushClient : IWebPushClient { // default TTL is 4 weeks. private const int DefaultTtl = 2419200; @@ -122,10 +124,156 @@ public void SetVapidDetails(string subject, string publicKey, string privateKey) /// 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); + // 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) + // { + // 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")) + // { + // var gcmApiKey = options["gcmAPIKey"] as string; + + // currentGcmApiKey = gcmApiKey ?? throw new ArgumentException("options.gcmAPIKey must be of type string"); + // } + + // 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")) + // { + // 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; + // } + // } + + // string? cryptoKeyHeader = null; + // request.Headers.Add("TTL", timeToLive.ToString()); + + // foreach (var header in 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 + // { + // request.Content = new ByteArrayContent(new byte[0]); + // request.Content.Headers.ContentLength = 0; + // } + + // var isGcm = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send"); + // var isFcm = subscription.Endpoint.StartsWith(@"https://fcm.googleapis.com/fcm/send/"); + + // if (isGcm) + // { + // if (!string.IsNullOrEmpty(currentGcmApiKey)) + // { + // request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); + // } + // } + // else if (currentVapidDetails != null) + // { + // var uri = new Uri(subscription.Endpoint); + // var audience = uri.Scheme + @"://" + uri.Host; + + // var vapidHeaders = VapidHelper.GetVapidHeaders(audience, currentVapidDetails.Subject, + // currentVapidDetails.PublicKey, currentVapidDetails.PrivateKey, currentVapidDetails.Expiration); + // request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); + // if (string.IsNullOrEmpty(cryptoKeyHeader)) + // { + // cryptoKeyHeader = vapidHeaders["Crypto-Key"]; + // } + // else + // { + // cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"]; + // } + // } + // else if (isFcm && !string.IsNullOrEmpty(currentGcmApiKey)) + // { + // request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); + // } + + // request.Headers.Add("Crypto-Key", cryptoKeyHeader); + // return request; + } + + /// + /// 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"); + throw new ArgumentException(@"You must pass in a subscription with at least a valid endpoint", nameof(subscription)); } var request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint); @@ -134,73 +282,50 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string.IsNullOrEmpty(subscription.P256DH))) { throw new ArgumentException( - @"To send a message with a payload, the subscription must have 'auth' and 'p256dh' keys."); + @"To send a message with a payload, the subscription must have 'auth' and 'p256dh' keys.", nameof(subscription)); } - var currentGcmApiKey = _gcmApiKey; - var currentVapidDetails = _vapidDetails; - var timeToLive = DefaultTtl; - var extraHeaders = new Dictionary(); - - if (options != null) + if (options is not null) { - var validOptionsKeys = new List { "headers", "gcmAPIKey", "vapidDetails", "TTL" }; - foreach (var key in options.Keys) + if (options.Topic is not null) { - if (!validOptionsKeys.Contains(key)) + if (string.IsNullOrWhiteSpace(options.Topic) || options.Topic.Length > 32) { - throw new ArgumentException(key + " is an invalid options. The valid options are" + - string.Join(",", validOptionsKeys)); + 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("headers")) - { - var headers = options["headers"] as Dictionary; - - extraHeaders = headers ?? throw new ArgumentException("options.headers must be of type Dictionary"); - } - - if (options.ContainsKey("gcmAPIKey")) - { - var gcmApiKey = options["gcmAPIKey"] as string; - - currentGcmApiKey = gcmApiKey ?? throw new ArgumentException("options.gcmAPIKey must be of type string"); - } - - 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")) - { - var ttl = options["TTL"] as int?; - if (ttl == null) + if (!RegexVariableName().IsMatch(options.Topic)) { - throw new ArgumentException("options.TTL must be of type int"); + throw new ArgumentException("options.topic uses unsupported characters set, use the URL or filename-safe Base64 characters set", nameof(options)); } - - //at this stage ttl cannot be null. - timeToLive = (int)ttl; } } string? cryptoKeyHeader = null; - request.Headers.Add("TTL", timeToLive.ToString()); + 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) { - request.Headers.Add(header.Key, header.Value.ToString()); + foreach (var header in options.ExtraHeaders) + { + request.Headers.Add(header.Key, header.Value.ToString()); + } } + var contentEncoding = options?.ContentEncoding ?? WebPushOptions.DefaultContentEncoding; 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"); + @"Unable to send a message with payload to this subscription since it doesn't have the required encryption key", nameof(subscription)); } var encryptedPayload = EncryptPayload(subscription, payload); @@ -208,49 +333,42 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, 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()); + request.Content.Headers.ContentEncoding.Add(contentEncoding.ToKebabCaseLower()); + if (contentEncoding == ContentEncoding.Aesgcm) + { + request.Headers.Add("Encryption", "salt=" + encryptedPayload.Base64EncodeSalt()); + } cryptoKeyHeader = @"dh=" + encryptedPayload.Base64EncodePublicKey(); } else { - request.Content = new ByteArrayContent(new byte[0]); + request.Content = new ByteArrayContent([]); request.Content.Headers.ContentLength = 0; } - var isGcm = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send"); - var isFcm = subscription.Endpoint.StartsWith(@"https://fcm.googleapis.com/fcm/send/"); - - if (isGcm) - { - if (!string.IsNullOrEmpty(currentGcmApiKey)) - { - request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); - } - } - else if (currentVapidDetails != null) + 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, currentVapidDetails.Subject, - currentVapidDetails.PublicKey, currentVapidDetails.PrivateKey, currentVapidDetails.Expiration); + var vapidHeaders = VapidHelper.GetVapidHeaders(audience, vapidDetails.Subject, vapidDetails.PublicKey, vapidDetails.PrivateKey, vapidDetails.Expiration, contentEncoding); request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); - if (string.IsNullOrEmpty(cryptoKeyHeader)) + if (contentEncoding == ContentEncoding.Aesgcm) { - cryptoKeyHeader = vapidHeaders["Crypto-Key"]; - } - else - { - cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"]; + if (string.IsNullOrEmpty(cryptoKeyHeader)) + { + cryptoKeyHeader = vapidHeaders["Crypto-Key"]; + } + else + { + cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"]; + } } } - else if (isFcm && !string.IsNullOrEmpty(currentGcmApiKey)) + if (contentEncoding == ContentEncoding.Aesgcm) { - request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); + request.Headers.Add("Crypto-Key", cryptoKeyHeader); } - - request.Headers.Add("Crypto-Key", cryptoKeyHeader); return request; } @@ -271,6 +389,21 @@ private static EncryptionResult EncryptPayload(PushSubscription subscription, st } } + /// + /// 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 @@ -313,6 +446,24 @@ public void SendNotification(PushSubscription subscription, string payload, stri 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); + + await HandleResponse(response, subscription).ConfigureAwait(false); + } /// /// To send a push notification asynchronous call this method with a subscription, optional payload and any options @@ -401,7 +552,7 @@ private static async Task HandleResponse(HttpResponseMessage response, PushSubsc string? details = null; if (response.Content != null) { - details = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + details = await response.Content.ReadAsStringAsync().ConfigureAwait(false); } var message = string.IsNullOrEmpty(details) @@ -411,6 +562,35 @@ private static async Task HandleResponse(HttpResponseMessage response, PushSubsc throw new WebPushException(message, subscription, response); } + 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 "gcmAPIKey": + // TODO... + break; + 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"); + } + } + return wpo; + } + public void Dispose() { if (_httpClient != null && _isHttpClientInternallyCreated) @@ -419,4 +599,7 @@ public void Dispose() _httpClient = null; } } + + [GeneratedRegex(@"^[A-Za-z0-9\-_]+$", RegexOptions.None, matchTimeoutMilliseconds: 100)] + private static partial Regex RegexVariableName(); } \ No newline at end of file From 7c669a6fca11609ef3385c6742ea4b750502d1b5 Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:08:30 +0200 Subject: [PATCH 05/10] Remove GCM key - Mark calls with dictionary parameter as obsolete --- WebPush/IWebPushClient.cs | 36 ++----- WebPush/WebPushClient.cs | 194 +------------------------------------- 2 files changed, 11 insertions(+), 219 deletions(-) diff --git a/WebPush/IWebPushClient.cs b/WebPush/IWebPushClient.cs index 1dc2171..716d517 100644 --- a/WebPush/IWebPushClient.cs +++ b/WebPush/IWebPushClient.cs @@ -8,14 +8,6 @@ namespace WebPush; 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 @@ -41,10 +33,11 @@ public interface IWebPushClient : IDisposable /// 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 + /// 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); @@ -80,9 +73,10 @@ HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? /// 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 + /// 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); @@ -95,15 +89,6 @@ void SendNotification(PushSubscription subscription, string? payload = null, /// 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 - /// The GCM API key - void SendNotification(PushSubscription subscription, string payload, string gcmApiKey); - /// /// To send a push notification asynchronous call this method with a subscription, optional payload and any options /// Will exception if unsuccessful @@ -124,10 +109,11 @@ void SendNotification(PushSubscription subscription, string? payload = null, /// 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 + /// 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); @@ -141,14 +127,4 @@ Task SendNotificationAsync(PushSubscription subscription, string? payload = null /// 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 GCM API key - /// - Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs index 26e9851..6ac3b7a 100644 --- a/WebPush/WebPushClient.cs +++ b/WebPush/WebPushClient.cs @@ -17,11 +17,8 @@ namespace WebPush; public partial class WebPushClient : IWebPushClient { - // default TTL is 4 weeks. - private const int DefaultTtl = 2419200; private readonly HttpClientHandler? _httpClientHandler; - private string? _gcmApiKey; private HttpClient? _httpClient; private VapidDetails? _vapidDetails; @@ -61,28 +58,6 @@ protected HttpClient 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; - } - - if (string.IsNullOrEmpty(gcmApiKey)) - { - throw new ArgumentException(@"The GCM API Key should be a non-empty string or null."); - } - - _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 @@ -118,7 +93,7 @@ public void SetVapidDetails(string subject, string publicKey, string privateKey) /// 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 + /// Options for vapid keys can be passed in if they are unique for each /// notification. /// /// A HttpRequestMessage object that can be sent. @@ -127,135 +102,6 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, { var wpo = ConvertOptions(options); return GenerateRequestDetails(subscription, payload, wpo); - // 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) - // { - // 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")) - // { - // var gcmApiKey = options["gcmAPIKey"] as string; - - // currentGcmApiKey = gcmApiKey ?? throw new ArgumentException("options.gcmAPIKey must be of type string"); - // } - - // 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")) - // { - // 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; - // } - // } - - // string? cryptoKeyHeader = null; - // request.Headers.Add("TTL", timeToLive.ToString()); - - // foreach (var header in 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 - // { - // request.Content = new ByteArrayContent(new byte[0]); - // request.Content.Headers.ContentLength = 0; - // } - - // var isGcm = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send"); - // var isFcm = subscription.Endpoint.StartsWith(@"https://fcm.googleapis.com/fcm/send/"); - - // if (isGcm) - // { - // if (!string.IsNullOrEmpty(currentGcmApiKey)) - // { - // request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); - // } - // } - // else if (currentVapidDetails != null) - // { - // var uri = new Uri(subscription.Endpoint); - // var audience = uri.Scheme + @"://" + uri.Host; - - // var vapidHeaders = VapidHelper.GetVapidHeaders(audience, currentVapidDetails.Subject, - // currentVapidDetails.PublicKey, currentVapidDetails.PrivateKey, currentVapidDetails.Expiration); - // request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); - // if (string.IsNullOrEmpty(cryptoKeyHeader)) - // { - // cryptoKeyHeader = vapidHeaders["Crypto-Key"]; - // } - // else - // { - // cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"]; - // } - // } - // else if (isFcm && !string.IsNullOrEmpty(currentGcmApiKey)) - // { - // request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey); - // } - - // request.Headers.Add("Crypto-Key", cryptoKeyHeader); - // return request; } /// @@ -411,7 +257,7 @@ public void SendNotification(PushSubscription subscription, string? payload = nu /// 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 + /// Options for vapid keys can be passed in if they are unique for each /// notification. /// public void SendNotification(PushSubscription subscription, string? payload = null, @@ -429,20 +275,7 @@ public void SendNotification(PushSubscription subscription, string? payload = nu /// 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 - /// The GCM API key - public void SendNotification(PushSubscription subscription, string payload, string gcmApiKey) - { - var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey }; + var options = new WebPushOptions { VapidDetails = vapidDetails, }; SendNotification(subscription, payload, options); } @@ -472,7 +305,7 @@ public async Task SendNotificationAsync(PushSubscription subscription, string? p /// 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 + /// Options for vapid keys can be passed in if they are unique for each /// notification. /// /// The cancellation token to cancel operation. @@ -496,21 +329,7 @@ public async Task SendNotificationAsync(PushSubscription subscription, string? p public async Task SendNotificationAsync(PushSubscription subscription, string payload, VapidDetails vapidDetails, CancellationToken cancellationToken = default) { - var options = new Dictionary { ["vapidDetails"] = vapidDetails }; - await SendNotificationAsync(subscription, payload, options, 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 - /// The GCM API key - /// - public async Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey, CancellationToken cancellationToken = default) - { - var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey }; + var options = new WebPushOptions { VapidDetails = vapidDetails, }; await SendNotificationAsync(subscription, payload, options, cancellationToken).ConfigureAwait(false); } @@ -573,9 +392,6 @@ private static WebPushOptions ConvertOptions(Dictionary? options var ttl = option.Value as int? ?? throw new ArgumentException("options.TTL must be of type int"); wpo.TTL = ttl; break; - case "gcmAPIKey": - // TODO... - break; case "vapidDetails": var vapids = option.Value as VapidDetails ?? throw new ArgumentException("options.vapidDetails must be of type VapidDetails"); wpo.VapidDetails = vapids; From b826659fa42fbeee6f942e95aaa7cfaa24f16802 Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:24:00 +0200 Subject: [PATCH 06/10] Adapt tests --- WebPush.Test/ECKeyHelperTest.cs | 113 +++++----- WebPush.Test/JWSSignerTest.cs | 95 +++++---- WebPush.Test/UrlBase64Test.cs | 39 ++-- WebPush.Test/VapidHelperTest.cs | 328 +++++++++++++++++++----------- WebPush.Test/WebPush.Test.csproj | 3 +- WebPush.Test/WebPushClientTest.cs | 313 +++++++++++++--------------- WebPush.Test/packages.lock.json | 198 ------------------ 7 files changed, 490 insertions(+), 599 deletions(-) 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..c9bea08 100755 --- a/WebPush.Test/WebPush.Test.csproj +++ b/WebPush.Test/WebPush.Test.csproj @@ -1,8 +1,9 @@  - 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..822b224 100644 --- a/WebPush.Test/WebPushClientTest.cs +++ b/WebPush.Test/WebPushClientTest.cs @@ -1,185 +1,144 @@ -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() + { + httpMessageHandlerMock = new MockHttpMessageHandler(); + client = new WebPushClient(httpMessageHandlerMock.ToHttpClient()); + } + + [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) { - 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"); - } + 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()); } -} \ No newline at end of file + +} diff --git a/WebPush.Test/packages.lock.json b/WebPush.Test/packages.lock.json index ead2608..7a3d432 100644 --- a/WebPush.Test/packages.lock.json +++ b/WebPush.Test/packages.lock.json @@ -1,204 +1,6 @@ { "version": 1, "dependencies": { - ".NETFramework,Version=v4.8": { - "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==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" - } - }, - "Moq": { - "type": "Direct", - "requested": "[4.20.72, )", - "resolved": "4.20.72", - "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", - "dependencies": { - "Castle.Core": "5.1.1", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "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", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, - "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": { - "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", - "dependencies": { - "System.Diagnostics.DiagnosticSource": "5.0.0" - } - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" - }, - "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==" - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.Collections.Immutable": { - "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.Memory": { - "type": "Transitive", - "resolved": "4.5.5", - "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", - "dependencies": { - "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.5.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "System.Numerics.Vectors": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" - }, - "System.Reflection.Metadata": { - "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.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" - } - }, - "webpush": { - "type": "Project" - } - }, "net10.0": { "Microsoft.NET.Test.Sdk": { "type": "Direct", From d443bbefc5c345880aaa2ad8141f408ae1d5e64b Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:44:57 +0200 Subject: [PATCH 07/10] Update Microsoft.NET.Test.Sdk --- WebPush.Test/WebPush.Test.csproj | 2 +- WebPush.Test/packages.lock.json | 72 ++++++++++++++++---------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/WebPush.Test/WebPush.Test.csproj b/WebPush.Test/WebPush.Test.csproj index c9bea08..80c7476 100755 --- a/WebPush.Test/WebPush.Test.csproj +++ b/WebPush.Test/WebPush.Test.csproj @@ -7,7 +7,7 @@ - + diff --git a/WebPush.Test/packages.lock.json b/WebPush.Test/packages.lock.json index 7a3d432..002e48e 100644 --- a/WebPush.Test/packages.lock.json +++ b/WebPush.Test/packages.lock.json @@ -4,12 +4,12 @@ "net10.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": { @@ -62,8 +62,8 @@ }, "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", @@ -151,15 +151,15 @@ }, "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" } }, @@ -189,12 +189,12 @@ "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": { @@ -247,8 +247,8 @@ }, "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", @@ -336,15 +336,15 @@ }, "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" } }, @@ -374,12 +374,12 @@ "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": { @@ -432,8 +432,8 @@ }, "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", @@ -521,15 +521,15 @@ }, "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" } }, From db6d8de952c77b7a271f891db84af6699e1b5d1b Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:54:02 +0200 Subject: [PATCH 08/10] Fix net8 exception --- WebPush.Test/WebPushClientTest.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WebPush.Test/WebPushClientTest.cs b/WebPush.Test/WebPushClientTest.cs index 822b224..4f9052c 100644 --- a/WebPush.Test/WebPushClientTest.cs +++ b/WebPush.Test/WebPushClientTest.cs @@ -128,8 +128,12 @@ public void TestHandlingFailureMessages(HttpStatusCode status, string response, public void TestHandleInvalidPublicKeys(int charactersToDrop) { var invalidKey = TestPublicKey.Substring(0, TestPublicKey.Length - charactersToDrop); - +#if NET8_0 + Assert.Throws(() => TestSendNotification(HttpStatusCode.OK, response: null, invalidKey)); +#endif +#if NET9_0_OR_GREATER Assert.ThrowsExactly(() => TestSendNotification(HttpStatusCode.OK, response: null, invalidKey)); +#endif } private void TestSendNotification(HttpStatusCode status, string response = null, string publicKey = TestPublicKey) From f351470d0891b02c213c52827ccbeac06e2a9f0b Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:01:27 +0200 Subject: [PATCH 09/10] Fix net8 test case --- WebPush.Test/WebPushClientTest.cs | 5 ----- WebPush/WebPushClient.cs | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/WebPush.Test/WebPushClientTest.cs b/WebPush.Test/WebPushClientTest.cs index 4f9052c..e556758 100644 --- a/WebPush.Test/WebPushClientTest.cs +++ b/WebPush.Test/WebPushClientTest.cs @@ -128,12 +128,7 @@ public void TestHandlingFailureMessages(HttpStatusCode status, string response, public void TestHandleInvalidPublicKeys(int charactersToDrop) { var invalidKey = TestPublicKey.Substring(0, TestPublicKey.Length - charactersToDrop); -#if NET8_0 - Assert.Throws(() => TestSendNotification(HttpStatusCode.OK, response: null, invalidKey)); -#endif -#if NET9_0_OR_GREATER Assert.ThrowsExactly(() => TestSendNotification(HttpStatusCode.OK, response: null, invalidKey)); -#endif } private void TestSendNotification(HttpStatusCode status, string response = null, string publicKey = TestPublicKey) diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs index 6ac3b7a..5782675 100644 --- a/WebPush/WebPushClient.cs +++ b/WebPush/WebPushClient.cs @@ -5,6 +5,7 @@ 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; @@ -226,7 +227,7 @@ private static EncryptionResult EncryptPayload(PushSubscription subscription, st } catch (Exception ex) { - if (ex is FormatException || ex is ArgumentException) + if (ex is FormatException || ex is ArgumentException || ex is CryptographicException) { throw new InvalidEncryptionDetailsException("Unable to encrypt the payload with the encryption key of this subscription.", subscription); } From 68cc46212045da577f6ede7112fbb90c58fb8248 Mon Sep 17 00:00:00 2001 From: Robert Bill <147130488+RobSlgm@users.noreply.github.com> Date: Mon, 4 May 2026 06:55:18 +0200 Subject: [PATCH 10/10] Update dependencies --- WebPush.Test/WebPush.Test.csproj | 4 +- WebPush.Test/packages.lock.json | 246 +++++++++++++++---------------- WebPush/WebPush.csproj | 4 +- WebPush/packages.lock.json | 78 +++++----- 4 files changed, 166 insertions(+), 166 deletions(-) diff --git a/WebPush.Test/WebPush.Test.csproj b/WebPush.Test/WebPush.Test.csproj index 80c7476..2fc9cd4 100755 --- a/WebPush.Test/WebPush.Test.csproj +++ b/WebPush.Test/WebPush.Test.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/WebPush.Test/packages.lock.json b/WebPush.Test/packages.lock.json index 002e48e..02cdb60 100644 --- a/WebPush.Test/packages.lock.json +++ b/WebPush.Test/packages.lock.json @@ -23,22 +23,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": { @@ -80,73 +80,73 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.17.0" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.17.0" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.IdentityModel.Logging": "8.17.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": { @@ -165,8 +165,8 @@ }, "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", @@ -181,8 +181,8 @@ "webpush": { "type": "Project", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "[8.17.0, )", - "Microsoft.IdentityModel.Tokens": "[8.17.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )", + "Microsoft.IdentityModel.Tokens": "[8.18.0, )" } } }, @@ -208,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": { @@ -265,73 +265,73 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.17.0" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.17.0" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.IdentityModel.Logging": "8.17.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": { @@ -350,8 +350,8 @@ }, "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", @@ -366,8 +366,8 @@ "webpush": { "type": "Project", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "[8.17.0, )", - "Microsoft.IdentityModel.Tokens": "[8.17.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )", + "Microsoft.IdentityModel.Tokens": "[8.18.0, )" } } }, @@ -393,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": { @@ -450,73 +450,73 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.17.0" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.17.0" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.IdentityModel.Logging": "8.17.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": { @@ -535,8 +535,8 @@ }, "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", @@ -551,8 +551,8 @@ "webpush": { "type": "Project", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "[8.17.0, )", - "Microsoft.IdentityModel.Tokens": "[8.17.0, )" + "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )", + "Microsoft.IdentityModel.Tokens": "[8.18.0, )" } } } diff --git a/WebPush/WebPush.csproj b/WebPush/WebPush.csproj index 213ca97..2315ca1 100755 --- a/WebPush/WebPush.csproj +++ b/WebPush/WebPush.csproj @@ -21,8 +21,8 @@ - - + + diff --git a/WebPush/packages.lock.json b/WebPush/packages.lock.json index ba63072..4c08e8e 100644 --- a/WebPush/packages.lock.json +++ b/WebPush/packages.lock.json @@ -4,21 +4,21 @@ "net10.0": { "Microsoft.IdentityModel.JsonWebTokens": { "type": "Direct", - "requested": "[8.17.0, )", - "resolved": "8.17.0", - "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.17.0" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, "Microsoft.IdentityModel.Tokens": { "type": "Direct", - "requested": "[8.17.0, )", - "resolved": "8.17.0", - "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.IdentityModel.Logging": "8.17.0" + "Microsoft.IdentityModel.Logging": "8.18.0" } }, "Microsoft.NET.ILLink.Tasks": { @@ -61,15 +61,15 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.17.0" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, "Microsoft.SourceLink.Common": { @@ -86,21 +86,21 @@ "net8.0": { "Microsoft.IdentityModel.JsonWebTokens": { "type": "Direct", - "requested": "[8.17.0, )", - "resolved": "8.17.0", - "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.17.0" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, "Microsoft.IdentityModel.Tokens": { "type": "Direct", - "requested": "[8.17.0, )", - "resolved": "8.17.0", - "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.IdentityModel.Logging": "8.17.0" + "Microsoft.IdentityModel.Logging": "8.18.0" } }, "Microsoft.NET.ILLink.Tasks": { @@ -143,15 +143,15 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.17.0" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, "Microsoft.SourceLink.Common": { @@ -168,21 +168,21 @@ "net9.0": { "Microsoft.IdentityModel.JsonWebTokens": { "type": "Direct", - "requested": "[8.17.0, )", - "resolved": "8.17.0", - "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.17.0" + "Microsoft.IdentityModel.Tokens": "8.18.0" } }, "Microsoft.IdentityModel.Tokens": { "type": "Direct", - "requested": "[8.17.0, )", - "resolved": "8.17.0", - "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "requested": "[8.18.0, )", + "resolved": "8.18.0", + "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.IdentityModel.Logging": "8.17.0" + "Microsoft.IdentityModel.Logging": "8.18.0" } }, "Microsoft.NET.ILLink.Tasks": { @@ -225,15 +225,15 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + "resolved": "8.18.0", + "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ==" }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.17.0", - "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "resolved": "8.18.0", + "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.17.0" + "Microsoft.IdentityModel.Abstractions": "8.18.0" } }, "Microsoft.SourceLink.Common": {