有没有办法在 .net 中进行 WebPush 有效负载加密?
Is there a way for WebPush payload encryption in .net?
我设法通过类似的方法向 chrome 和 firefox 发送空的推送通知,虽然我试图使我的通知更详细,但是我找不到使用 . net 作为后台。
我的firefox示例如下:
Shared Function sendPushFox(ByVal value As String) As String
Dim toret As String = ""
Dim query As String = "SELECT subscribeid FROM custom_user_data WHERE NOT subscribeid = ' ';"
Dim connection As New MySqlConnection(Utils.connectionString) : connection.Open()
Dim command As MySqlCommand = New MySqlCommand(query, connection)
Dim reader As MySqlDataReader = command.ExecuteReader()
Dim regList As New List(Of String)
Do While reader.Read
regList.Add(reader.GetString(0))
Loop
Dim query2 As String = "SELECT p256dh FROM custom_user_data WHERE NOT p256dh = ' ';"
Dim connection2 As New MySqlConnection(Utils.connectionString) : connection2.Open()
Dim command2 As MySqlCommand = New MySqlCommand(query2, connection2)
Dim reader2 As MySqlDataReader = command2.ExecuteReader()
Dim regList2 As New List(Of String)
Do While reader.Read
regList2.Add(reader.GetString(0))
Loop
Dim query3 As String = "SELECT authsecret FROM custom_user_data WHERE NOT authsecret = ' ';"
Dim connection3 As New MySqlConnection(Utils.connectionString) : connection3.Open()
Dim command3 As MySqlCommand = New MySqlCommand(query3, connection3)
Dim reader3 As MySqlDataReader = command3.ExecuteReader()
Dim regList3 As New List(Of String)
Do While reader.Read
regList3.Add(reader.GetString(0))
Loop
Dim reg1 = regList.ToArray
Dim reg2 = regList2.ToArray
Dim reg3 = regList3.ToArray
For Each Element As String In reg1
Try
Dim tRequest As WebRequest
tRequest = WebRequest.Create("https://updates.push.services.mozilla.com/push/v1/" & Element)
tRequest.Method = "post"
tRequest.ContentType = " application/json"
tRequest.Headers.Add("TTL: 1800")
tRequest.Headers.Add("payload: " + value)
For Each p25key As String In reg2
tRequest.Headers.Add("userPublicKey: " + p25key)
Next
For Each authkey As String In reg3
tRequest.Headers.Add("userAuth: " + authkey)
Next
Dim dataStream As Stream = tRequest.GetRequestStream()
Dim tResponse As WebResponse = tRequest.GetResponse()
dataStream = tResponse.GetResponseStream()
Dim tReader As New StreamReader(dataStream)
Dim sResponseFromServer As [String] = tReader.ReadToEnd()
toret = sResponseFromServer
tReader.Close()
dataStream.Close()
tResponse.Close()
Catch ex As Exception
Console.WriteLine(ex.Message)
Continue For
End Try
Next
Return toret
End Function
userPublicKey 或 userAuth 现在都没有实际使用,并且在没有有效负载加密的情况下没有任何用途,所以我已经阅读过,但是使用 vb.net,没有 .net 库用于向网络平台(chrome 和 FF 浏览器)发送推送通知,但我找不到任何例子,所以我有点卡住了。
如您所见,我已将每个客户端的端点、p256dh 和 auth 保存到 mysql 数据库中,但从那时起我一直无法取得进展。
看来有人想办法做到这一点。使用 BouncyCastle from this blog:
复制解决方案
/*
* Built for .NET Core 1.0 on Windows 10 with Portable.BouncyCastle v1.8.1.1
*
* Tested on Chrome v53.0.2785.113 m (64-bit) and Firefox 48.0.2
*
* Massive thanks to Peter Beverloo for the following:
* https://docs.google.com/document/d/1_kWRLJHRYN0KH73WipFyfIXI1UzZ5IyOYSs-y_mLxEE/
* https://tests.peter.sh/push-encryption-verifier/
*
* Some more useful links:
* https://developers.google.com/web/updates/2016/03/web-push-encryption?hl=en
* https://github.com/web-push-libs/web-push/blob/master/src/index.js
*
* Copyright (C) 2016 BravoTango86
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using Microsoft.AspNetCore.WebUtilities;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Security;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
public class WebPushHelper {
private const string FirebaseServerKey = "";
public static bool SendNotification(JsonSubscription sub, byte[] data, int ttl = 0, ushort padding = 0,
bool randomisePadding = false) {
return SendNotification(endpoint: sub.endpoint,
data: data,
userKey: WebEncoders.Base64UrlDecode(sub.keys["p256dh"]),
userSecret: WebEncoders.Base64UrlDecode(sub.keys["auth"]),
ttl: ttl,
padding: padding,
randomisePadding: randomisePadding);
}
public static bool SendNotification(string endpoint, string data, string userKey, string userSecret,
int ttl = 0, ushort padding = 0, bool randomisePadding = false) {
return SendNotification(endpoint: endpoint,
data: Encoding.UTF8.GetBytes(data),
userKey: WebEncoders.Base64UrlDecode(userKey),
userSecret: WebEncoders.Base64UrlDecode(userSecret),
ttl: ttl,
padding: padding,
randomisePadding: randomisePadding);
}
public static bool SendNotification(string endpoint, byte[] userKey, byte[] userSecret, byte[] data = null,
int ttl = 0, ushort padding = 0, bool randomisePadding = false) {
HttpRequestMessage Request = new HttpRequestMessage(HttpMethod.Post, endpoint);
if (endpoint.StartsWith("https://android.googleapis.com/gcm/send/"))
Request.Headers.TryAddWithoutValidation("Authorization", "key=" + FirebaseServerKey);
Request.Headers.Add("TTL", ttl.ToString());
if (data != null && userKey != null && userSecret != null) {
EncryptionResult Package = EncryptMessage(userKey, userSecret, data, padding, randomisePadding);
Request.Content = new ByteArrayContent(Package.Payload);
Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
Request.Content.Headers.ContentLength = Package.Payload.Length;
Request.Content.Headers.ContentEncoding.Add("aesgcm");
Request.Headers.Add("Crypto-Key", "keyid=p256dh;dh=" + WebEncoders.Base64UrlEncode(Package.PublicKey));
Request.Headers.Add("Encryption", "keyid=p256dh;salt=" + WebEncoders.Base64UrlEncode(Package.Salt));
}
using (HttpClient HC = new HttpClient()) {
return HC.SendAsync(Request).Result.StatusCode == HttpStatusCode.Created;
}
}
public static EncryptionResult EncryptMessage(byte[] userKey, byte[] userSecret, byte[] data,
ushort padding = 0, bool randomisePadding = false) {
SecureRandom Random = new SecureRandom();
byte[] Salt = new byte[16];
Random.NextBytes(Salt);
X9ECParameters Curve = ECNamedCurveTable.GetByName("prime256v1");
ECDomainParameters Spec = new ECDomainParameters(Curve.Curve, Curve.G, Curve.N, Curve.H, Curve.GetSeed());
ECKeyPairGenerator Generator = new ECKeyPairGenerator();
Generator.Init(new ECKeyGenerationParameters(Spec, new SecureRandom()));
AsymmetricCipherKeyPair KeyPair = Generator.GenerateKeyPair();
ECDHBasicAgreement AgreementGenerator = new ECDHBasicAgreement();
AgreementGenerator.Init(KeyPair.Private);
BigInteger IKM = AgreementGenerator.CalculateAgreement(new ECPublicKeyParameters(Spec.Curve.DecodePoint(userKey), Spec));
byte[] PRK = GenerateHKDF(userSecret, IKM.ToByteArrayUnsigned(), Encoding.UTF8.GetBytes("Content-Encoding: auth[=10=]"), 32);
byte[] PublicKey = ((ECPublicKeyParameters)KeyPair.Public).Q.GetEncoded(false);
byte[] CEK = GenerateHKDF(Salt, PRK, CreateInfoChunk("aesgcm", userKey, PublicKey), 16);
byte[] Nonce = GenerateHKDF(Salt, PRK, CreateInfoChunk("nonce", userKey, PublicKey), 12);
if (randomisePadding && padding > 0) padding = Convert.ToUInt16(Math.Abs(Random.NextInt()) % (padding + 1));
byte[] Input = new byte[padding + 2 + data.Length];
Buffer.BlockCopy(ConvertInt(padding), 0, Input, 0, 2);
Buffer.BlockCopy(data, 0, Input, padding + 2, data.Length);
IBufferedCipher Cipher = CipherUtilities.GetCipher("AES/GCM/NoPadding");
Cipher.Init(true, new AeadParameters(new KeyParameter(CEK), 128, Nonce));
byte[] Message = new byte[Cipher.GetOutputSize(Input.Length)];
Cipher.DoFinal(Input, 0, Input.Length, Message, 0);
return new EncryptionResult() { Salt = Salt, Payload = Message, PublicKey = PublicKey };
}
public class EncryptionResult {
public byte[] PublicKey { get; set; }
public byte[] Payload { get; set; }
public byte[] Salt { get; set; }
}
public class JsonSubscription {
public string endpoint { get; set; }
public Dictionary<string, string> keys { get; set; }
}
public static byte[] ConvertInt(int number) {
byte[] Output = BitConverter.GetBytes(Convert.ToUInt16(number));
if (BitConverter.IsLittleEndian) Array.Reverse(Output);
return Output;
}
public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey) {
List<byte> Output = new List<byte>();
Output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}[=10=]P-256[=10=]"));
Output.AddRange(ConvertInt(recipientPublicKey.Length));
Output.AddRange(recipientPublicKey);
Output.AddRange(ConvertInt(senderPublicKey.Length));
Output.AddRange(senderPublicKey);
return Output.ToArray();
}
public static byte[] GenerateHKDF(byte[] salt, byte[] ikm, byte[] info, int len) {
IMac PRKGen = MacUtilities.GetMac("HmacSHA256");
PRKGen.Init(new KeyParameter(MacUtilities.CalculateMac("HmacSHA256", new KeyParameter(salt), ikm)));
PRKGen.BlockUpdate(info, 0, info.Length);
PRKGen.Update((byte)1);
byte[] Result = MacUtilities.DoFinal(PRKGen);
if (Result.Length > len) Array.Resize(ref Result, len);
return Result;
}
}
要使此代码与 asp.net 一起工作,请将 aspcore 命名空间中的 decode/encode 方法替换为:
///<summary>
/// Base 64 Encoding with URL and Filename Safe Alphabet using UTF-8 character set.
///</summary>
///<param name="str">The origianl string</param>
///<returns>The Base64 encoded string</returns>
public static string Base64ForUrlEncode(string str)
{
byte[] encbuff = Encoding.UTF8.GetBytes(str);
return HttpServerUtility.UrlTokenEncode(encbuff);
}
///<summary>
/// Decode Base64 encoded string with URL and Filename Safe Alphabet using UTF-8.
///</summary>
///<param name="str">Base64 code</param>
///<returns>The decoded string.</returns>
public static string Base64ForUrlDecode(string str)
{
byte[] decbuff = HttpServerUtility.UrlTokenDecode(str);
return Encoding.UTF8.GetString(decbuff);
}
我设法通过类似的方法向 chrome 和 firefox 发送空的推送通知,虽然我试图使我的通知更详细,但是我找不到使用 . net 作为后台。
我的firefox示例如下:
Shared Function sendPushFox(ByVal value As String) As String
Dim toret As String = ""
Dim query As String = "SELECT subscribeid FROM custom_user_data WHERE NOT subscribeid = ' ';"
Dim connection As New MySqlConnection(Utils.connectionString) : connection.Open()
Dim command As MySqlCommand = New MySqlCommand(query, connection)
Dim reader As MySqlDataReader = command.ExecuteReader()
Dim regList As New List(Of String)
Do While reader.Read
regList.Add(reader.GetString(0))
Loop
Dim query2 As String = "SELECT p256dh FROM custom_user_data WHERE NOT p256dh = ' ';"
Dim connection2 As New MySqlConnection(Utils.connectionString) : connection2.Open()
Dim command2 As MySqlCommand = New MySqlCommand(query2, connection2)
Dim reader2 As MySqlDataReader = command2.ExecuteReader()
Dim regList2 As New List(Of String)
Do While reader.Read
regList2.Add(reader.GetString(0))
Loop
Dim query3 As String = "SELECT authsecret FROM custom_user_data WHERE NOT authsecret = ' ';"
Dim connection3 As New MySqlConnection(Utils.connectionString) : connection3.Open()
Dim command3 As MySqlCommand = New MySqlCommand(query3, connection3)
Dim reader3 As MySqlDataReader = command3.ExecuteReader()
Dim regList3 As New List(Of String)
Do While reader.Read
regList3.Add(reader.GetString(0))
Loop
Dim reg1 = regList.ToArray
Dim reg2 = regList2.ToArray
Dim reg3 = regList3.ToArray
For Each Element As String In reg1
Try
Dim tRequest As WebRequest
tRequest = WebRequest.Create("https://updates.push.services.mozilla.com/push/v1/" & Element)
tRequest.Method = "post"
tRequest.ContentType = " application/json"
tRequest.Headers.Add("TTL: 1800")
tRequest.Headers.Add("payload: " + value)
For Each p25key As String In reg2
tRequest.Headers.Add("userPublicKey: " + p25key)
Next
For Each authkey As String In reg3
tRequest.Headers.Add("userAuth: " + authkey)
Next
Dim dataStream As Stream = tRequest.GetRequestStream()
Dim tResponse As WebResponse = tRequest.GetResponse()
dataStream = tResponse.GetResponseStream()
Dim tReader As New StreamReader(dataStream)
Dim sResponseFromServer As [String] = tReader.ReadToEnd()
toret = sResponseFromServer
tReader.Close()
dataStream.Close()
tResponse.Close()
Catch ex As Exception
Console.WriteLine(ex.Message)
Continue For
End Try
Next
Return toret
End Function
userPublicKey 或 userAuth 现在都没有实际使用,并且在没有有效负载加密的情况下没有任何用途,所以我已经阅读过,但是使用 vb.net,没有 .net 库用于向网络平台(chrome 和 FF 浏览器)发送推送通知,但我找不到任何例子,所以我有点卡住了。
如您所见,我已将每个客户端的端点、p256dh 和 auth 保存到 mysql 数据库中,但从那时起我一直无法取得进展。
看来有人想办法做到这一点。使用 BouncyCastle from this blog:
复制解决方案/*
* Built for .NET Core 1.0 on Windows 10 with Portable.BouncyCastle v1.8.1.1
*
* Tested on Chrome v53.0.2785.113 m (64-bit) and Firefox 48.0.2
*
* Massive thanks to Peter Beverloo for the following:
* https://docs.google.com/document/d/1_kWRLJHRYN0KH73WipFyfIXI1UzZ5IyOYSs-y_mLxEE/
* https://tests.peter.sh/push-encryption-verifier/
*
* Some more useful links:
* https://developers.google.com/web/updates/2016/03/web-push-encryption?hl=en
* https://github.com/web-push-libs/web-push/blob/master/src/index.js
*
* Copyright (C) 2016 BravoTango86
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using Microsoft.AspNetCore.WebUtilities;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Agreement;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Security;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
public class WebPushHelper {
private const string FirebaseServerKey = "";
public static bool SendNotification(JsonSubscription sub, byte[] data, int ttl = 0, ushort padding = 0,
bool randomisePadding = false) {
return SendNotification(endpoint: sub.endpoint,
data: data,
userKey: WebEncoders.Base64UrlDecode(sub.keys["p256dh"]),
userSecret: WebEncoders.Base64UrlDecode(sub.keys["auth"]),
ttl: ttl,
padding: padding,
randomisePadding: randomisePadding);
}
public static bool SendNotification(string endpoint, string data, string userKey, string userSecret,
int ttl = 0, ushort padding = 0, bool randomisePadding = false) {
return SendNotification(endpoint: endpoint,
data: Encoding.UTF8.GetBytes(data),
userKey: WebEncoders.Base64UrlDecode(userKey),
userSecret: WebEncoders.Base64UrlDecode(userSecret),
ttl: ttl,
padding: padding,
randomisePadding: randomisePadding);
}
public static bool SendNotification(string endpoint, byte[] userKey, byte[] userSecret, byte[] data = null,
int ttl = 0, ushort padding = 0, bool randomisePadding = false) {
HttpRequestMessage Request = new HttpRequestMessage(HttpMethod.Post, endpoint);
if (endpoint.StartsWith("https://android.googleapis.com/gcm/send/"))
Request.Headers.TryAddWithoutValidation("Authorization", "key=" + FirebaseServerKey);
Request.Headers.Add("TTL", ttl.ToString());
if (data != null && userKey != null && userSecret != null) {
EncryptionResult Package = EncryptMessage(userKey, userSecret, data, padding, randomisePadding);
Request.Content = new ByteArrayContent(Package.Payload);
Request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
Request.Content.Headers.ContentLength = Package.Payload.Length;
Request.Content.Headers.ContentEncoding.Add("aesgcm");
Request.Headers.Add("Crypto-Key", "keyid=p256dh;dh=" + WebEncoders.Base64UrlEncode(Package.PublicKey));
Request.Headers.Add("Encryption", "keyid=p256dh;salt=" + WebEncoders.Base64UrlEncode(Package.Salt));
}
using (HttpClient HC = new HttpClient()) {
return HC.SendAsync(Request).Result.StatusCode == HttpStatusCode.Created;
}
}
public static EncryptionResult EncryptMessage(byte[] userKey, byte[] userSecret, byte[] data,
ushort padding = 0, bool randomisePadding = false) {
SecureRandom Random = new SecureRandom();
byte[] Salt = new byte[16];
Random.NextBytes(Salt);
X9ECParameters Curve = ECNamedCurveTable.GetByName("prime256v1");
ECDomainParameters Spec = new ECDomainParameters(Curve.Curve, Curve.G, Curve.N, Curve.H, Curve.GetSeed());
ECKeyPairGenerator Generator = new ECKeyPairGenerator();
Generator.Init(new ECKeyGenerationParameters(Spec, new SecureRandom()));
AsymmetricCipherKeyPair KeyPair = Generator.GenerateKeyPair();
ECDHBasicAgreement AgreementGenerator = new ECDHBasicAgreement();
AgreementGenerator.Init(KeyPair.Private);
BigInteger IKM = AgreementGenerator.CalculateAgreement(new ECPublicKeyParameters(Spec.Curve.DecodePoint(userKey), Spec));
byte[] PRK = GenerateHKDF(userSecret, IKM.ToByteArrayUnsigned(), Encoding.UTF8.GetBytes("Content-Encoding: auth[=10=]"), 32);
byte[] PublicKey = ((ECPublicKeyParameters)KeyPair.Public).Q.GetEncoded(false);
byte[] CEK = GenerateHKDF(Salt, PRK, CreateInfoChunk("aesgcm", userKey, PublicKey), 16);
byte[] Nonce = GenerateHKDF(Salt, PRK, CreateInfoChunk("nonce", userKey, PublicKey), 12);
if (randomisePadding && padding > 0) padding = Convert.ToUInt16(Math.Abs(Random.NextInt()) % (padding + 1));
byte[] Input = new byte[padding + 2 + data.Length];
Buffer.BlockCopy(ConvertInt(padding), 0, Input, 0, 2);
Buffer.BlockCopy(data, 0, Input, padding + 2, data.Length);
IBufferedCipher Cipher = CipherUtilities.GetCipher("AES/GCM/NoPadding");
Cipher.Init(true, new AeadParameters(new KeyParameter(CEK), 128, Nonce));
byte[] Message = new byte[Cipher.GetOutputSize(Input.Length)];
Cipher.DoFinal(Input, 0, Input.Length, Message, 0);
return new EncryptionResult() { Salt = Salt, Payload = Message, PublicKey = PublicKey };
}
public class EncryptionResult {
public byte[] PublicKey { get; set; }
public byte[] Payload { get; set; }
public byte[] Salt { get; set; }
}
public class JsonSubscription {
public string endpoint { get; set; }
public Dictionary<string, string> keys { get; set; }
}
public static byte[] ConvertInt(int number) {
byte[] Output = BitConverter.GetBytes(Convert.ToUInt16(number));
if (BitConverter.IsLittleEndian) Array.Reverse(Output);
return Output;
}
public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey) {
List<byte> Output = new List<byte>();
Output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}[=10=]P-256[=10=]"));
Output.AddRange(ConvertInt(recipientPublicKey.Length));
Output.AddRange(recipientPublicKey);
Output.AddRange(ConvertInt(senderPublicKey.Length));
Output.AddRange(senderPublicKey);
return Output.ToArray();
}
public static byte[] GenerateHKDF(byte[] salt, byte[] ikm, byte[] info, int len) {
IMac PRKGen = MacUtilities.GetMac("HmacSHA256");
PRKGen.Init(new KeyParameter(MacUtilities.CalculateMac("HmacSHA256", new KeyParameter(salt), ikm)));
PRKGen.BlockUpdate(info, 0, info.Length);
PRKGen.Update((byte)1);
byte[] Result = MacUtilities.DoFinal(PRKGen);
if (Result.Length > len) Array.Resize(ref Result, len);
return Result;
}
}
要使此代码与 asp.net 一起工作,请将 aspcore 命名空间中的 decode/encode 方法替换为:
///<summary>
/// Base 64 Encoding with URL and Filename Safe Alphabet using UTF-8 character set.
///</summary>
///<param name="str">The origianl string</param>
///<returns>The Base64 encoded string</returns>
public static string Base64ForUrlEncode(string str)
{
byte[] encbuff = Encoding.UTF8.GetBytes(str);
return HttpServerUtility.UrlTokenEncode(encbuff);
}
///<summary>
/// Decode Base64 encoded string with URL and Filename Safe Alphabet using UTF-8.
///</summary>
///<param name="str">Base64 code</param>
///<returns>The decoded string.</returns>
public static string Base64ForUrlDecode(string str)
{
byte[] decbuff = HttpServerUtility.UrlTokenDecode(str);
return Encoding.UTF8.GetString(decbuff);
}