有没有办法在 .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);
}