Skip to content

Commit f986ab9

Browse files
Webpush crypto refac
1 parent 7fdfc0e commit f986ab9

2 files changed

Lines changed: 30 additions & 40 deletions

File tree

Backend/Altafraner.AfraApp/Otium/Services/EnrollmentService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using Altafraner.AfraApp.User.Domain.DTO;
1313
using Altafraner.AfraApp.User.Domain.Models;
1414
using Altafraner.AfraApp.User.Services;
15-
using Altafraner.Backbone.EmailSchedulingModule;
1615
using Altafraner.Backbone.Utils;
1716
using Microsoft.EntityFrameworkCore;
1817
using Models_OtiumEinschreibung = Altafraner.AfraApp.Otium.Domain.Models.OtiumEinschreibung;

Backend/Altafraner.Backbone.WebNotification/Services/WebPushSender.cs

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Security.Cryptography;
33
using System.Text;
44
using System.Text.Json;
5+
using System.Buffers.Text;
56
using Microsoft.Extensions.Logging;
67
using 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

Comments
 (0)