【问题标题】:Is there a way for WebPush payload encryption in .net?有没有办法在.net 中进行 WebPush 有效负载加密?
【发布时间】:2023-03-06 16:59:02
【问题描述】:

我设法通过类似的方法向 chrome 和 firefox 发送空推送通知,尽管我试图使我的通知更详细,但是我找不到使用 .net 作为后端的详细 webpush 通知的示例。

我的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 库到web 平台(chrome 和 FF 浏览器),我在任何地方都找不到示例,所以我有点卡住了。

如您所见,我将端点、来自每个客户端的 p256dh 和 auth 保存到 mysql 数据库中,但从那时起我一直无法取得进展。

【问题讨论】:

    标签: c# .net vb.net push-notification web-push


    【解决方案1】:

    似乎有人找到了这样做的方法。使用 BouncyCastlethis 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\0"), 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}\0P-256\0"));
            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);
    }
    

    【讨论】:

    • 这太棒了,非常感谢。我一直在继续使用难以描述的推送通知,因为我从来没有时间弄清楚。军团士兵的照片也不错,我以前经常玩HoN哈哈。
    • 当我遇到这个问题时,我找到了许多语言的解决方案......除了 C#。这让我很困扰,直到找到解决方案我才停下来。
    • 我正在使用asp.net,但我不明白最后一部分。替换这些方法是什么意思?它们是否必须在特定的类上实现?我应该将第一段代码“WebEncoders.Base64UrlDecode”和“WebEncoders.Base64UrlEncode”的调用替换为这些方法吗?谢谢。
    • @JohnnyClara 是的,你做对了。您应该将第一段代码“WebEncoders.Base64UrlDecode”和“WebEncoders.Base64UrlEncode”中的调用替换为这些方法。这是因为 WebEncoders 类位于 AspNetCore 命名空间中,这在旧版本的 asp.net 中不可用。
    猜你喜欢
    • 2019-07-24
    • 2021-01-16
    • 2016-03-12
    • 1970-01-01
    • 2011-01-09
    • 2021-04-11
    • 1970-01-01
    • 1970-01-01
    • 2012-09-23
    相关资源
    最近更新 更多