Skip to content

Commit 71afc98

Browse files
Merge pull request #102 from andrei-m-code/remove-bouncycastle-upgrade-net10
Remove BouncyCastle, upgrade to .NET 10, fix bugs
2 parents 6294c05 + 899de97 commit 71afc98

7 files changed

Lines changed: 105 additions & 110 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: Setup .NET
1414
uses: actions/setup-dotnet@v4
1515
with:
16-
dotnet-version: '9.x'
16+
dotnet-version: '10.x'
1717

1818
- name: Restore
1919
run: dotnet restore CorePush.sln

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: Setup .NET
1414
uses: actions/setup-dotnet@v4
1515
with:
16-
dotnet-version: '9.x'
16+
dotnet-version: '10.x'
1717

1818
- name: Restore
1919
run: dotnet restore CorePush.sln

CorePush.Tester/CorePush.Tester.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net9.0</TargetFramework>
5+
<TargetFramework>net10.0</TargetFramework>
66
</PropertyGroup>
77

88
<ItemGroup>
@@ -11,7 +11,6 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="FirebaseAdmin" Version="3.4.0" />
14-
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />
1514
</ItemGroup>
1615

1716
</Project>

CorePush.Tester/Program.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,13 @@ private static async Task SendApnNotificationAsync()
5050
ServerType = apnServerType,
5151
};
5252

53-
while (true)
54-
{
55-
var apn = new ApnSender(settings, http);
56-
var payload = new AppleNotification(
57-
Guid.NewGuid(),
58-
"Hello World (Message)",
59-
"Hello World (Title)");
60-
var response = await apn.SendAsync(payload, apnDeviceToken);
61-
}
53+
var apn = new ApnSender(settings, http);
54+
var payload = new AppleNotification(
55+
Guid.NewGuid(),
56+
"Hello World (Message)",
57+
"Hello World (Title)");
58+
var response = await apn.SendAsync(payload, apnDeviceToken);
59+
Console.WriteLine($"APN Response: {response.StatusCode} - {response.Message}");
6260
}
6361

6462
private static async Task SendFirebaseNotificationAsync()

CorePush/Apple/ApnSender.cs

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Net.Http;
@@ -7,8 +7,6 @@
77
using System.Text;
88
using System.Threading;
99
using System.Threading.Tasks;
10-
using Org.BouncyCastle.Crypto.Parameters;
11-
using Org.BouncyCastle.Security;
1210

1311
using CorePush.Interfaces;
1412
using CorePush.Models;
@@ -43,7 +41,7 @@ public class ApnSender : IApnSender
4341
public ApnSender(ApnSettings settings, HttpClient http) : this(settings, http, new DefaultCorePushJsonSerializer())
4442
{
4543
}
46-
44+
4745
/// <summary>
4846
/// Apple push notification sender constructor
4947
/// </summary>
@@ -55,7 +53,7 @@ public ApnSender(ApnSettings settings, HttpClient http, IJsonSerializer serializ
5553
this.settings = settings ?? throw new ArgumentNullException(nameof(settings));
5654
this.http = http ?? throw new ArgumentNullException(nameof(http));
5755
this.serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
58-
56+
5957
if (http.BaseAddress == null)
6058
{
6159
http.BaseAddress = new Uri(servers[settings.ServerType]);
@@ -65,7 +63,7 @@ public ApnSender(ApnSettings settings, HttpClient http, IJsonSerializer serializ
6563
/// <summary>
6664
/// Serialize and send notification to APN. Please see how your message should be formatted here:
6765
/// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW1
68-
/// !IMPORTANT: If you send many messages at once, make sure to retry those calls. Apple typically doesn't like
66+
/// !IMPORTANT: If you send many messages at once, make sure to retry those calls. Apple typically doesn't like
6967
/// to receive too many requests and may occasionally respond with HTTP 429. Just try/catch this call and retry as needed.
7068
/// </summary>
7169
/// <exception cref="HttpRequestException">Throws exception when not successful</exception>
@@ -78,14 +76,16 @@ public async Task<PushResult> SendAsync(
7876
ApnPushType apnPushType = ApnPushType.Alert,
7977
CancellationToken cancellationToken = default)
8078
{
79+
ArgumentException.ThrowIfNullOrWhiteSpace(deviceToken);
80+
8181
var path = $"/3/device/{deviceToken}";
8282
var json = serializer.Serialize(notification);
8383

8484
using var message = new HttpRequestMessage(HttpMethod.Post, path);
85-
85+
8686
message.Version = new Version(2, 0);
8787
message.Content = new StringContent(json);
88-
88+
8989
message.Headers.Authorization = new AuthenticationHeaderValue("bearer", GetJwtToken());
9090
message.Headers.TryAddWithoutValidation(":method", "POST");
9191
message.Headers.TryAddWithoutValidation(":path", path);
@@ -100,21 +100,22 @@ public async Task<PushResult> SendAsync(
100100
}
101101

102102
using var response = await http.SendAsync(message, cancellationToken);
103-
103+
104104
var content = await response.Content.ReadAsStringAsync(cancellationToken);
105-
var error = response.IsSuccessStatusCode
106-
? null
107-
: serializer.Deserialize<ApnsError>(content).Reason;
105+
var error = response.IsSuccessStatusCode
106+
? null
107+
: serializer.Deserialize<ApnsError>(content)?.Reason;
108108

109109
return new PushResult((int)response.StatusCode, response.IsSuccessStatusCode, content, error);
110110
}
111111

112112
private string GetJwtToken()
113113
{
114-
var (token, date) = tokens.GetOrAdd(settings.AppBundleIdentifier, _ => new Tuple<string, DateTime>(CreateJwtToken(), DateTime.UtcNow));
114+
var cacheKey = $"{settings.AppBundleIdentifier}:{settings.P8PrivateKeyId}";
115+
var (token, date) = tokens.GetOrAdd(cacheKey, _ => new Tuple<string, DateTime>(CreateJwtToken(), DateTime.UtcNow));
115116
if (date < DateTime.UtcNow.AddMinutes(-tokenExpiresMinutes))
116117
{
117-
tokens.TryRemove(settings.AppBundleIdentifier, out _);
118+
tokens.TryRemove(cacheKey, out _);
118119
return GetJwtToken();
119120
}
120121

@@ -129,32 +130,26 @@ private string CreateJwtToken()
129130
var payloadBase64 = Base64UrlEncode(payload);
130131
var unsignedJwtData = $"{headerBase64}.{payloadBase64}";
131132
var unsignedJwtBytes = Encoding.UTF8.GetBytes(unsignedJwtData);
132-
133+
133134
var privateKeyBytes = Convert.FromBase64String(CryptoHelper.CleanP8Key(settings.P8PrivateKey));
134-
var keyParams = (ECPrivateKeyParameters) PrivateKeyFactory.CreateKey(privateKeyBytes);
135-
var q = keyParams.Parameters.G.Multiply(keyParams.D).Normalize();
136-
137-
using var dsa = ECDsa.Create(new ECParameters
138-
{
139-
Curve = ECCurve.CreateFromValue(keyParams.PublicKeyParamSet.Id),
140-
D = keyParams.D.ToByteArrayUnsigned(),
141-
Q =
142-
{
143-
X = q.XCoord.GetEncoded(),
144-
Y = q.YCoord.GetEncoded()
145-
}
146-
});
147-
148-
var signature = dsa.SignData(unsignedJwtBytes, 0, unsignedJwtBytes.Length, HashAlgorithmName.SHA256);
135+
136+
using var dsa = ECDsa.Create();
137+
dsa.ImportPkcs8PrivateKey(privateKeyBytes, out _);
138+
139+
var signature = dsa.SignData(unsignedJwtBytes, HashAlgorithmName.SHA256);
149140
var signatureBase64 = Base64UrlEncode(signature);
150141
return $"{unsignedJwtData}.{signatureBase64}";
151142
}
152-
143+
153144
private static string Base64UrlEncode(string str)
154145
{
155146
var bytes = Encoding.UTF8.GetBytes(str);
156147
return Base64UrlEncode(bytes);
157148
}
158149

159-
private static string Base64UrlEncode(byte[] bytes) => Convert.ToBase64String(bytes);
150+
private static string Base64UrlEncode(byte[] bytes) =>
151+
Convert.ToBase64String(bytes)
152+
.Replace('+', '-')
153+
.Replace('/', '_')
154+
.TrimEnd('=');
160155
}

CorePush/CorePush.csproj

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net9.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
66

77
<Title>Server Side library for sending ✅Web, ✅Android and ✅iOS Push Notifications</Title>
@@ -25,6 +25,15 @@
2525
<PackageTags>push-notifications android-push-notifications ios-push-notifications web-push web-push-notifications apn fcm firebase</PackageTags>
2626

2727
<PackageReleaseNotes>
28+
v5.0.0
29+
- Remove BouncyCastle dependency in favor of built-in .NET cryptography
30+
- Upgrade to .NET 10
31+
- Fix Base64URL encoding for JWT tokens (RFC 7617)
32+
- Fix null reference in APN error handling
33+
- Fix token cache key collision
34+
- Add device token validation
35+
- Propagate CancellationToken in Firebase sender
36+
2837
v4.3.0, v4.4.0
2938
- Upgrade to Bouncycastle.Cryptography
3039
- Other pkg version bumps
@@ -81,10 +90,6 @@ v3.0.0
8190

8291
</PropertyGroup>
8392

84-
<ItemGroup>
85-
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
86-
</ItemGroup>
87-
8893
<ItemGroup>
8994
<None Include="..\Icon.png" Pack="true" PackagePath="Icon.png" />
9095
<None Include="..\README.md" Pack="true" PackagePath="README.md" />

0 commit comments

Comments
 (0)