22using System . Security . Cryptography ;
33using System . Text ;
44using System . Text . Json ;
5+ using System . Buffers . Text ;
56using Microsoft . Extensions . Logging ;
67using Microsoft . Extensions . Options ;
78
@@ -25,9 +26,9 @@ internal sealed class WebPushSender<TPerson> where TPerson : class, IWebNotifica
2526
2627 private readonly string _subject = string . Empty ;
2728 private readonly string _publicKeyBase64Url = string . Empty ; // original URL-safe base64 (65 bytes)
28- private readonly byte [ ] _publicKeyX = [ ] ; // 32-byte X coordinate
29- private readonly byte [ ] _publicKeyY = [ ] ; // 32-byte Y coordinate
30- private readonly byte [ ] _privateKeyD = [ ] ; // 32-byte private scalar
29+
30+ private readonly ECDsa _ecdsa = null ! ;
31+ private readonly ECParameters _ecParams ;
3132
3233 /// <summary>
3334 /// Constructs a new <see cref="WebPushSender{TPerson}" />.
@@ -52,8 +53,8 @@ public WebPushSender(
5253 byte [ ] privKeyBytes ;
5354 try
5455 {
55- pubKeyBytes = Base64UrlDecode ( cfg . PublicKey ) ;
56- privKeyBytes = Base64UrlDecode ( cfg . PrivateKey ) ;
56+ pubKeyBytes = Base64Url . DecodeFromUtf8 ( Encoding . UTF8 . GetBytes ( cfg . PublicKey ) ) ;
57+ privKeyBytes = Base64Url . DecodeFromUtf8 ( Encoding . UTF8 . GetBytes ( cfg . PrivateKey ) ) ;
5758 }
5859 catch ( FormatException ex )
5960 {
@@ -69,9 +70,23 @@ public WebPushSender(
6970 "VAPID:PrivateKey must be a 32-byte P-256 private scalar (base64url-encoded)." ) ;
7071
7172 _publicKeyBase64Url = cfg . PublicKey ;
72- _publicKeyX = pubKeyBytes [ 1 ..33 ] ;
73- _publicKeyY = pubKeyBytes [ 33 ..65 ] ;
74- _privateKeyD = privKeyBytes ;
73+ var publicKeyX = pubKeyBytes [ 1 ..33 ] ;
74+ var publicKeyY = pubKeyBytes [ 33 ..65 ] ;
75+ var privateKeyD = privKeyBytes ;
76+
77+ _ecdsa = ECDsa . Create ( new ECParameters
78+ {
79+ Curve = ECCurve . NamedCurves . nistP256 ,
80+ Q = new ECPoint { X = publicKeyX , Y = publicKeyY } ,
81+ D = privateKeyD ,
82+ } ) ;
83+ _ecParams = new ECParameters
84+ {
85+ Curve = ECCurve . NamedCurves . nistP256 ,
86+ Q = new ECPoint { X = publicKeyX , Y = publicKeyY } ,
87+ D = privateKeyD ,
88+ } ;
89+
7590 _subject = cfg . Subject ?? string . Empty ;
7691 IsEnabled = true ;
7792 }
@@ -91,8 +106,8 @@ public async Task SendAsync(Uri endpoint, string p256dhBase64Url, string authBas
91106 if ( ! IsEnabled )
92107 return ;
93108
94- var uaPublicKey = Base64UrlDecode ( p256dhBase64Url ) ;
95- var authSecret = Base64UrlDecode ( authBase64Url ) ;
109+ var uaPublicKey = Base64Url . DecodeFromUtf8 ( Encoding . UTF8 . GetBytes ( p256dhBase64Url ) ) ;
110+ var authSecret = Base64Url . DecodeFromUtf8 ( Encoding . UTF8 . GetBytes ( authBase64Url ) ) ;
96111
97112 var body = EncryptPayload ( Encoding . UTF8 . GetBytes ( jsonPayload ) , uaPublicKey , authSecret ) ;
98113
@@ -130,26 +145,19 @@ public async Task SendAsync(Uri endpoint, string p256dhBase64Url, string authBas
130145
131146 private string CreateVapidJwt ( string audience )
132147 {
133- var header = Base64UrlEncode ( """{"typ":"JWT","alg":"ES256"}"""u8 . ToArray ( ) ) ;
148+ var header = Base64Url . EncodeToString ( """{"typ":"JWT","alg":"ES256"}"""u8 . ToArray ( ) ) ;
134149 var exp = DateTimeOffset . UtcNow . AddHours ( 12 ) . ToUnixTimeSeconds ( ) ;
135- var payload = Base64UrlEncode (
150+ var payload = Base64Url . EncodeToString (
136151 Encoding . UTF8 . GetBytes (
137152 JsonSerializer . Serialize ( new { aud = audience , exp , sub = _subject } ) ) ) ;
138153
139- var signingInput = Encoding . ASCII . GetBytes ( $ "{ header } .{ payload } ") ;
140- var ecParams = new ECParameters
141- {
142- Curve = ECCurve . NamedCurves . nistP256 ,
143- Q = new ECPoint { X = _publicKeyX , Y = _publicKeyY } ,
144- D = _privateKeyD ,
145- } ;
146- using var ecdsa = ECDsa . Create ( ecParams ) ;
154+ var signingInput = Encoding . UTF8 . GetBytes ( $ "{ header } .{ payload } ") ;
147155
148- var signature = ecdsa . SignData (
156+ var signature = _ecdsa . SignData (
149157 signingInput , HashAlgorithmName . SHA256 ,
150158 DSASignatureFormat . IeeeP1363FixedFieldConcatenation ) ;
151159
152- return $ "{ header } .{ payload } .{ Base64UrlEncode ( signature ) } ";
160+ return $ "{ header } .{ payload } .{ Base64Url . EncodeToString ( signature ) } ";
153161 }
154162
155163 private static byte [ ] EncryptPayload ( byte [ ] plaintext , byte [ ] uaPublicKey , byte [ ] authSecret )
@@ -228,21 +236,4 @@ private static string GetAudience(Uri uri)
228236 {
229237 return $ "{ uri . Scheme } ://{ uri . Authority } ";
230238 }
231-
232- internal static byte [ ] Base64UrlDecode ( string value )
233- {
234- try
235- {
236- var padding = ( 4 - value . Length % 4 ) % 4 ;
237- var base64 = value . Replace ( '-' , '+' ) . Replace ( '_' , '/' ) + new string ( '=' , padding ) ;
238- return Convert . FromBase64String ( base64 ) ;
239- }
240- catch ( FormatException ex )
241- {
242- throw new FormatException ( $ "The value '{ value } ' is not valid URL-safe base64.", ex ) ;
243- }
244- }
245-
246- internal static string Base64UrlEncode ( byte [ ] data )
247- => Convert . ToBase64String ( data ) . TrimEnd ( '=' ) . Replace ( '+' , '-' ) . Replace ( '/' , '_' ) ;
248239}
0 commit comments