-
-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
656 additions
and
3 deletions.
There are no files selected for viewing
239 changes: 239 additions & 0 deletions
239
...tCore.PushNotifications.Services.PushService/Client/Authentication/VapidAuthentication.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
using System; | ||
using System.Text; | ||
using System.Collections.Generic; | ||
using System.Security.Cryptography; | ||
using Newtonsoft.Json; | ||
using Org.BouncyCastle.Math; | ||
using Org.BouncyCastle.Crypto.Parameters; | ||
using Org.BouncyCastle.Crypto.Signers; | ||
using Demo.AspNetCore.PushNotifications.Services.PushService.Client.Internals; | ||
|
||
namespace Demo.AspNetCore.PushNotifications.Services.PushService.Client.Authentication | ||
{ | ||
internal class VapidAuthentication | ||
{ | ||
#region Structures | ||
public readonly struct WebPushSchemeHeadersValues | ||
{ | ||
public string AuthenticationHeaderValueParameter { get; } | ||
|
||
public string CryptoKeyHeaderValue { get; } | ||
|
||
public WebPushSchemeHeadersValues(string authenticationHeaderValueParameter, string cryptoKeyHeaderValue) | ||
: this() | ||
{ | ||
AuthenticationHeaderValueParameter = authenticationHeaderValueParameter; | ||
CryptoKeyHeaderValue = cryptoKeyHeaderValue; | ||
} | ||
} | ||
#endregion | ||
|
||
#region Fields | ||
private const string AUDIENCE_CLAIM = "aud"; | ||
private const string EXPIRATION_CLAIM = "exp"; | ||
private const string SUBJECT_CLAIM = "sub"; | ||
private const string P256ECDSA_PREFIX = "p256ecdsa="; | ||
private const string VAPID_AUTHENTICATION_HEADER_VALUE_PARAMETER_FORMAT = "t={0}, k={1}"; | ||
private const int DEFAULT_EXPIRATION = 43200; | ||
private const int MAXIMUM_EXPIRATION = 86400; | ||
|
||
private string _subject; | ||
private string _publicKey; | ||
private string _privateKey; | ||
private ECPrivateKeyParameters _privateSigningKey; | ||
private int _expiration; | ||
|
||
private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0); | ||
private static readonly Dictionary<string, string> _jwtHeader = new Dictionary<string, string> | ||
{ | ||
{ "typ", "JWT" }, | ||
{ "alg", "ES256" } | ||
}; | ||
#endregion | ||
|
||
#region Properties | ||
public string Subject | ||
{ | ||
get { return _subject; } | ||
|
||
set | ||
{ | ||
if (!String.IsNullOrWhiteSpace(value)) | ||
{ | ||
if (!value.StartsWith("mailto:")) | ||
{ | ||
if (!Uri.IsWellFormedUriString(value, UriKind.Absolute) || ((new Uri(value)).Scheme != Uri.UriSchemeHttps)) | ||
{ | ||
throw new ArgumentException(nameof(Subject), "Subject should include a contact URI for the application server as either a 'mailto: ' (email) or an 'https:' URI"); | ||
} | ||
} | ||
|
||
_subject = value; | ||
} | ||
else | ||
{ | ||
_subject = null; | ||
} | ||
} | ||
} | ||
|
||
public string PublicKey | ||
{ | ||
get { return _publicKey; } | ||
|
||
set | ||
{ | ||
if (String.IsNullOrWhiteSpace(value)) | ||
{ | ||
throw new ArgumentNullException(nameof(PublicKey)); | ||
} | ||
|
||
byte[] decodedPublicKey = UrlBase64Converter.FromUrlBase64String(value); | ||
if (decodedPublicKey.Length != 65) | ||
{ | ||
throw new ArgumentException(nameof(PublicKey), "VAPID public key must be 65 bytes long"); | ||
} | ||
|
||
_publicKey = value; | ||
} | ||
} | ||
|
||
public string PrivateKey | ||
{ | ||
get { return _privateKey; } | ||
|
||
set | ||
{ | ||
if (String.IsNullOrWhiteSpace(value)) | ||
{ | ||
throw new ArgumentNullException(nameof(PrivateKey)); | ||
} | ||
|
||
byte[] decodedPrivateKey = UrlBase64Converter.FromUrlBase64String(value); | ||
if (decodedPrivateKey.Length != 32) | ||
{ | ||
throw new ArgumentException(nameof(PrivateKey), "VAPID private key should be 32 bytes long"); | ||
} | ||
|
||
_privateKey = value; | ||
_privateSigningKey = ECKeyHelper.GetECPrivateKeyParameters(decodedPrivateKey); | ||
} | ||
} | ||
|
||
public int Expiration | ||
{ | ||
get { return _expiration; } | ||
|
||
set | ||
{ | ||
if ((value <= 0) || (value > MAXIMUM_EXPIRATION)) | ||
{ | ||
throw new ArgumentOutOfRangeException(nameof(Expiration), "Expiration must be a number of seconds not longer than 24 hours"); | ||
} | ||
|
||
_expiration = value; | ||
} | ||
} | ||
#endregion | ||
|
||
#region Constructor | ||
public VapidAuthentication(string publicKey, string privateKey) | ||
{ | ||
PublicKey = publicKey; | ||
PrivateKey = privateKey; | ||
|
||
_expiration = DEFAULT_EXPIRATION; | ||
} | ||
#endregion | ||
|
||
#region Methods | ||
public string GetVapidSchemeAuthenticationHeaderValueParameter(string audience) | ||
{ | ||
return String.Format(VAPID_AUTHENTICATION_HEADER_VALUE_PARAMETER_FORMAT, GetToken(audience), _publicKey); | ||
} | ||
|
||
public WebPushSchemeHeadersValues GetWebPushSchemeHeadersValues(string audience) | ||
{ | ||
return new WebPushSchemeHeadersValues(GetToken(audience), P256ECDSA_PREFIX + _publicKey); | ||
} | ||
|
||
private string GetToken(string audience) | ||
{ | ||
if (String.IsNullOrWhiteSpace(audience)) | ||
{ | ||
throw new ArgumentNullException(nameof(audience)); | ||
} | ||
|
||
if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute)) | ||
{ | ||
throw new ArgumentException(nameof(audience), "Audience should be an absolute URL"); | ||
} | ||
|
||
Dictionary<string, object> jwtBody = GetJwtBody(audience); | ||
|
||
return GenerateJwtToken(_jwtHeader, jwtBody); | ||
} | ||
|
||
private Dictionary<string, object> GetJwtBody(string audience) | ||
{ | ||
Dictionary<string, object> jwtBody = new Dictionary<string, object> | ||
{ | ||
{ AUDIENCE_CLAIM, audience }, | ||
{ EXPIRATION_CLAIM, GetAbsoluteExpiration(_expiration) } | ||
}; | ||
|
||
if (_subject != null) | ||
{ | ||
jwtBody.Add(SUBJECT_CLAIM, _subject); | ||
} | ||
|
||
return jwtBody; | ||
} | ||
|
||
private static long GetAbsoluteExpiration(int expirationSeconds) | ||
{ | ||
TimeSpan unixEpochOffset = DateTime.UtcNow - _unixEpoch; | ||
|
||
return (long)unixEpochOffset.TotalSeconds + expirationSeconds; | ||
} | ||
|
||
private string GenerateJwtToken(Dictionary<string, string> jwtHeader, Dictionary<string, object> jwtBody) | ||
{ | ||
string jwtInput = UrlBase64Converter.ToUrlBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtHeader))) | ||
+ "." | ||
+ UrlBase64Converter.ToUrlBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jwtBody))); | ||
|
||
byte[] jwtInputHash; | ||
using (var sha256Hasher = SHA256.Create()) | ||
{ | ||
jwtInputHash = sha256Hasher.ComputeHash(Encoding.UTF8.GetBytes(jwtInput)); | ||
} | ||
|
||
ECDsaSigner jwtSigner = new ECDsaSigner(); | ||
jwtSigner.Init(true, _privateSigningKey); | ||
|
||
BigInteger[] jwtSignature = jwtSigner.GenerateSignature(jwtInputHash); | ||
|
||
byte[] jwtSignatureFirstSegment = jwtSignature[0].ToByteArrayUnsigned(); | ||
byte[] jwtSignatureSecondSegment = jwtSignature[1].ToByteArrayUnsigned(); | ||
|
||
int jwtSignatureSegmentLength = Math.Max(jwtSignatureFirstSegment.Length, jwtSignatureSecondSegment.Length); | ||
byte[] combinedJwtSignature = new byte[2 * jwtSignatureSegmentLength]; | ||
ByteArrayCopyWithPadLeft(jwtSignatureFirstSegment, combinedJwtSignature, 0, jwtSignatureSegmentLength); | ||
ByteArrayCopyWithPadLeft(jwtSignatureSecondSegment, combinedJwtSignature, jwtSignatureSegmentLength, jwtSignatureSegmentLength); | ||
|
||
return jwtInput + "." + UrlBase64Converter.ToUrlBase64String(combinedJwtSignature); | ||
} | ||
|
||
private static void ByteArrayCopyWithPadLeft(byte[] sourceArray, byte[] destinationArray, int destinationIndex, int destinationLengthToUse) | ||
{ | ||
if (sourceArray.Length != destinationLengthToUse) | ||
{ | ||
destinationIndex += (destinationLengthToUse - sourceArray.Length); | ||
} | ||
|
||
Array.Copy(sourceArray, 0, destinationArray, destinationIndex, sourceArray.Length); | ||
} | ||
#endregion | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
Demo.AspNetCore.PushNotifications.Services.PushService/Client/Internals/ECKeyHelper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
using System; | ||
using System.IO; | ||
using Org.BouncyCastle.Asn1; | ||
using Org.BouncyCastle.Asn1.Nist; | ||
using Org.BouncyCastle.Asn1.X9; | ||
using Org.BouncyCastle.Crypto; | ||
using Org.BouncyCastle.Crypto.Parameters; | ||
using Org.BouncyCastle.OpenSsl; | ||
using Org.BouncyCastle.Security; | ||
|
||
namespace Demo.AspNetCore.PushNotifications.Services.PushService.Client.Internals | ||
{ | ||
internal static class ECKeyHelper | ||
{ | ||
internal static ECPrivateKeyParameters GetECPrivateKeyParameters(byte[] privateKey) | ||
{ | ||
Asn1Object derSequence = new DerSequence( | ||
new DerInteger(1), | ||
new DerOctetString(privateKey), | ||
new DerTaggedObject(0, new DerObjectIdentifier("1.2.840.10045.3.1.7")) | ||
); | ||
|
||
string pemKey = "-----BEGIN EC PRIVATE KEY-----\n" | ||
+ Convert.ToBase64String(derSequence.GetDerEncoded()) | ||
+ "\n-----END EC PRIVATE KEY----"; | ||
|
||
PemReader pemKeyReader = new PemReader(new StringReader(pemKey)); | ||
AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemKeyReader.ReadObject(); | ||
|
||
return (ECPrivateKeyParameters)keyPair.Private; | ||
} | ||
|
||
internal static ECPublicKeyParameters GetECPublicKeyParameters(byte[] publicKey) | ||
{ | ||
Asn1Object derSequence = new DerSequence( | ||
new DerSequence(new DerObjectIdentifier(@"1.2.840.10045.2.1"), new DerObjectIdentifier(@"1.2.840.10045.3.1.7")), | ||
new DerBitString(publicKey) | ||
); | ||
|
||
string pemKey = "-----BEGIN PUBLIC KEY-----\n" | ||
+ Convert.ToBase64String(derSequence.GetDerEncoded()) | ||
+ "\n-----END PUBLIC KEY-----"; | ||
|
||
PemReader pemKeyReader = new PemReader(new StringReader(pemKey)); | ||
return (ECPublicKeyParameters)pemKeyReader.ReadObject(); | ||
} | ||
|
||
internal static AsymmetricCipherKeyPair GenerateAsymmetricCipherKeyPair() | ||
{ | ||
X9ECParameters ecParameters = NistNamedCurves.GetByName("P-256"); | ||
ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameters.Curve, ecParameters.G, ecParameters.N, ecParameters.H, ecParameters.GetSeed()); | ||
|
||
IAsymmetricCipherKeyPairGenerator keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator("ECDH"); | ||
keyPairGenerator.Init(new ECKeyGenerationParameters(ecDomainParameters, new SecureRandom())); | ||
|
||
return keyPairGenerator.GenerateKeyPair(); | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
....AspNetCore.PushNotifications.Services.PushService/Client/Internals/UrlBase64Converter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
using System; | ||
|
||
namespace Demo.AspNetCore.PushNotifications.Services.PushService.Client.Internals | ||
{ | ||
internal static class UrlBase64Converter | ||
{ | ||
internal static byte[] FromUrlBase64String(string input) | ||
{ | ||
input = input.Replace('-', '+').Replace('_', '/'); | ||
|
||
while (input.Length % 4 != 0) | ||
{ | ||
input += "="; | ||
} | ||
|
||
return Convert.FromBase64String(input); | ||
} | ||
|
||
internal static string ToUrlBase64String(byte[] input) | ||
{ | ||
return Convert.ToBase64String(input).Replace('+', '-').Replace('/', '_').TrimEnd('='); | ||
} | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
Demo.AspNetCore.PushNotifications.Services.PushService/Client/PushMessage.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
using System; | ||
|
||
namespace Demo.AspNetCore.PushNotifications.Services.PushService.Client | ||
{ | ||
internal class PushMessage | ||
{ | ||
#region Fields | ||
private int? _timeToLive; | ||
#endregion | ||
|
||
#region Properties | ||
public string Content { get; set; } | ||
|
||
public int? TimeToLive | ||
{ | ||
get { return _timeToLive; } | ||
|
||
set | ||
{ | ||
if (value.HasValue && (value.Value < 0)) | ||
{ | ||
throw new ArgumentOutOfRangeException(nameof(TimeToLive), "The TTL must be a non-negative integer"); | ||
} | ||
|
||
_timeToLive = value; | ||
} | ||
} | ||
#endregion | ||
|
||
#region Constructors | ||
public PushMessage(string content) | ||
{ | ||
Content = content; | ||
} | ||
#endregion | ||
} | ||
} |
Oops, something went wrong.