Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way for WebPush payload encryption in .net?

Tags:

I manage to send empty push notifications to chrome and firefox, through similar methods though I'm trying to make my notifications more detailed, however I can't find an example of detailed webpush notifications using .net as the backend.

My firefox example is as follows:

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

Neither the userPublicKey or userAuth are actually in use right now and don't serve any purpose without the encryption of the payload so I've read, however using vb.net, there is no .net library for sending push notifications to web platforms (chrome and FF browsers) and there's no examples I can find anywhere, so I'm a little bit stuck.

As you can see, I have the endpoint, the p256dh and auth from each client saved into a mysql DB, but from that point I've been unable to progress thus far.

like image 414
Christopher Butler Avatar asked Jul 02 '16 16:07

Christopher Butler


People also ask

How the payload encryption is done in rest APIS?

The encrypted payload is structured in JSON Web Encryption (JWE) format, the plain text JSON body is encrypted to form a JWE encrypted payload that is inserted into the request body(replacing the plain text data). The encryption process uses the appropriate public encryption key for the environment.

What is Web push protocol?

The web push protocol [ RFC8030 ] describes a protocol that enables communication between a user agent or application server and a push service.

What is vapid push notification?

VAPID, which stands for Voluntary Application Server Identity, is a new way to send and receive website push notifications. Your VAPID keys allow you to send web push campaigns without having to send them through a service like Firebase Cloud Messaging (or FCM).


1 Answers

It seems that someone find a way to do that. Copying solution using 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\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;
    }

}

To make this code work with asp.net replace decode/encode methods from aspcore namespace with:

///<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);
}
like image 130
LbISS Avatar answered Sep 28 '22 02:09

LbISS