packages feed

web-push 0.1.2.0 → 0.3

raw patch · 5 files changed

+203/−252 lines, 5 filesdep +lensdep +safe-exceptionsdep −exceptionsdep −josedep −mtldep ~cryptonitedep ~hspecdep ~http-client

Dependencies added: lens, safe-exceptions

Dependencies removed: exceptions, jose, mtl, unordered-containers

Dependency ranges changed: cryptonite, hspec, http-client, memory

Files

LICENSE view
README.md view
@@ -1,18 +1,18 @@-# web-push+# web-push [![Hackage](https://img.shields.io/hackage/v/web-push.svg)](https://hackage.haskell.org/package/web-push)  Helper functions to send messages using Web Push protocol.  ## Usage -The `sendPushNotification` function encodes the message into Base64 URL form before encrypting and sending. Decode the message in Service Worker notification handler in browser before trying to read the JSON message.+Guides to using Web Push API in browsers can be found on [Mozilla's](https://developer.mozilla.org/en/docs/Web/API/Push_API) and [Google's](https://developers.google.com/web/fundamentals/engage-and-retain/push-notifications/) docs, or you can check out the demo app in the example folder. To run the demo app: -Guides to using Web Push API in browsers can be found on [Mozilla's](https://developer.mozilla.org/en/docs/Web/API/Push_API) and [Google's](https://developers.google.com/web/fundamentals/engage-and-retain/push-notifications/) docs, or you can check out [this](https://gist.github.com/sarthakbagaria/c08c0d7b84e1165760bb429a4064cfff) _untested_ Yesod app demonstrating the use of this library.+    cd example+    stack build+    stack --docker-network=bridge --docker-run-args='--publish=3000:3000' exec web-push-example -## To Do+Then access localhost:3000 from a browser. Keep the browser console open to check if there are errors. For use with docker, the above command requires [stack](https://docs.haskellstack.org/en/stable/README/) 2.4 or above. -- Add recognition of more error/status codes from send notification HTTP response.-- Clearly differentiate between ByteString encodings (Raw, Base64URL etc).-- Extend tests to verify that push messages are sent properly.+For production use, store a set of VAPID keys securely and use them for all push notification subscriptions and messages; public key will have to be exposed to client's browser when subscribing to push notifications, but private key must be kept secret and used when generating push notifications on the server. If VAPID keys are re-generated, all push notifications will require re-subscriptions. Also save the latest subscription details such as endpoint from user's browser session securely in the database and use them to send push notifications to the user later.  ## References 
src/Web/WebPush.hs view
@@ -1,4 +1,7 @@ {-# LANGUAGE RecordWildCards, OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts, DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Web.WebPush     (@@ -7,64 +10,68 @@     , readVAPIDKeys     , vapidPublicKeyBytes     , sendPushNotification+    , pushEndpoint+    , pushP256dh+    , pushAuth+    , pushSenderEmail+    , pushExpireInSeconds+    , pushMessage+    , mkPushNotification     -- * Types     , VAPIDKeys     , VAPIDKeysMinDetails(..)-    , PushNotificationDetails(..)+    , PushNotification     , PushNotificationMessage(..)     , PushNotificationError(..)+    , PushEndpoint+    , PushP256dh+    , PushAuth     ) where   import Web.WebPush.Internal  import Crypto.Random                                           (MonadRandom(getRandomBytes))+import Control.Exception                                       (Exception)+import Control.Lens                                            ((^.), Lens', Lens, lens)+ import qualified Crypto.PubKey.ECC.Types         as ECC import qualified Crypto.PubKey.ECC.Generate      as ECC import qualified Crypto.PubKey.ECC.ECDSA         as ECDSA import qualified Crypto.PubKey.ECC.DH            as ECDH -import Crypto.JWT                                              (NumericDate(..))-import qualified Crypto.JWT                      as JWT- import qualified Data.Bits                       as Bits import Data.Word                                               (Word8) +import GHC.Generics                                            (Generic) import GHC.Int                                                 (Int64) import qualified Data.List                       as L-import Data.Text                                               (Text) import qualified Data.Text                       as T import qualified Data.Text.Encoding              as TE-import qualified Data.Text.Encoding.Error        as TE-import Data.Monoid                                             ((<>)) import qualified Data.ByteString.Lazy            as LB import qualified Data.ByteString                 as BS import qualified Data.ByteString.Char8           as C8 import qualified Data.Aeson                      as A import qualified Data.ByteString.Base64.URL      as B64.URL-import qualified Data.ByteString.Base64.URL.Lazy as B64.URL.Lazy --import Data.Time.Clock                                         (getCurrentTime)-import Data.Time                                               (addUTCTime)--import Network.HTTP.Client                                     (Manager, httpLbs, parseRequest, HttpException(HttpExceptionRequest), HttpExceptionContent(StatusCodeException), RequestBody(..), requestBody, requestHeaders, method, host, secure, responseStatus)+import Network.HTTP.Client                                     (Manager, httpLbs, parseUrlThrow, HttpException(HttpExceptionRequest)+                                                               , HttpExceptionContent(StatusCodeException), RequestBody(..)+                                                               , requestBody, requestHeaders, method, responseStatus) import Network.HTTP.Types                                      (hContentType, hAuthorization, hContentEncoding) import Network.HTTP.Types.Status                               (Status(statusCode)) -import qualified Crypto.JOSE.Error               as JOSE.Error import Crypto.Error                                            (CryptoError) -import Control.Exception.Base                                  (SomeException(..), fromException)-import Control.Monad.Catch.Pure                                (runCatchT) import Control.Monad.IO.Class                                  (MonadIO, liftIO)+import Control.Exception.Base                                  (SomeException(..), fromException, toException, throw)+import Control.Exception.Safe                                  (tryAny, handleAny)  import System.Random                                           (randomRIO)   -- |Generate the 3 integers minimally representing a unique pair of public and private keys. ----- Store them in configuration and use them across multiple push notification requests.+-- Store them securely and use them across multiple push notification requests. generateVAPIDKeys :: MonadRandom m => m VAPIDKeysMinDetails generateVAPIDKeys = do     -- SEC_p256r1 is the NIST P-256@@ -84,8 +91,9 @@   -- |Pass the VAPID public key bytes to browser when subscribing to push notifications.--- Generate application server key using--- applicationServerKey = new Uint8Array(vapidPublicKeyBytes) in Javascript.+-- Generate application server key browser using:+--+-- > applicationServerKey = new Uint8Array( #{toJSON vapidPublicKeyBytes} ) vapidPublicKeyBytes :: VAPIDKeys -> [Word8] vapidPublicKeyBytes keys =    let ECC.Point vapidPublicKeyX vapidPublicKeyY = ECDSA.public_q $ ECDSA.toPublicKey keys@@ -102,133 +110,150 @@                                               ([1..32] :: [Int])  --- References:--- NOTE: these references are drafts; the current implementation is based on the versions given below.--- https://tools.ietf.org/html/draft-ietf-webpush-encryption-04--- https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-02--- https://tools.ietf.org/html/draft-ietf-webpush-protocol-10--- https://tools.ietf.org/html/draft-ietf-webpush-vapid-01---- |Send a Push Message.--- The message sent is Base64 URL encoded.--- Decode the message in Service Worker notification handler in browser before trying to read the JSON.-sendPushNotification :: MonadIO m+-- |Send a Push Message. Read the message in Service Worker notification handler in browser:+--+-- > self.addEventListener('push', function(event){ console.log(event.data.json()); });+sendPushNotification :: (MonadIO m, A.ToJSON msg)                      => VAPIDKeys                      -> Manager-                     -> PushNotificationDetails+                     -> PushNotification msg                      -> m (Either PushNotificationError ()) sendPushNotification vapidKeys httpManager pushNotification = do-    eitherInitReq <- runCatchT $ parseRequest $ T.unpack $ endpoint pushNotification-    case eitherInitReq of-        Left exc@(SomeException _) -> return $ Left $ EndpointParseFailed exc-        Right initReq -> do-            time <- liftIO $ getCurrentTime-            eitherJwt <- webPushJWT vapidKeys $ VAPIDClaims { vapidAud =  JWT.Audience [ JWT.fromString $ TE.decodeUtf8With TE.lenientDecode $-                                                                                             BS.append (if secure initReq then "https://" else "http://") (host initReq)-                                                                                       ]-                                                            , vapidSub = JWT.fromString $ T.append "mailto:" $ senderEmail pushNotification-                                                            , vapidExp = NumericDate $ addUTCTime 3000 time-                                                            }-            case eitherJwt of-                Left err -> return $ Left $ JWTGenerationFailed err-                Right jwt -> do+    result <- liftIO $ tryAny $ do+        initReq <- handleAny (throw . EndpointParseFailed) $ parseUrlThrow . T.unpack $ pushNotification ^. pushEndpoint+        jwt <- webPushJWT vapidKeys initReq (pushNotification ^. pushSenderEmail)+        ecdhServerPrivateKey <- ECDH.generatePrivate $ ECC.getCurveByName ECC.SEC_p256r1+        randSalt <- getRandomBytes 16+        padLen <- randomRIO (0, 20) -                    ecdhServerPrivateKey <- liftIO $ ECDH.generatePrivate $ ECC.getCurveByName ECC.SEC_p256r1-                    randSalt <- liftIO $ getRandomBytes 16-                    padLen <- liftIO $ randomRIO (0, 20)+        let authSecretBytes = B64.URL.decodeLenient . TE.encodeUtf8 $ pushNotification ^. pushAuth+            -- extract the 65 bytes of ECDH uncompressed public key received from browser in subscription+            subscriptionPublicKeyBytes = B64.URL.decodeLenient . TE.encodeUtf8 $ pushNotification ^. pushP256dh+            -- encode the message to a safe representation like base64URL before sending it to encryption algorithms+            -- decode the message through service workers on browsers before trying to read the JSON+            plainMessage64Encoded = A.encode . A.toJSON $ pushNotification ^. pushMessage+            encryptionInput =+                EncryptionInput+                    { applicationServerPrivateKey = ecdhServerPrivateKey+                    , userAgentPublicKeyBytes = subscriptionPublicKeyBytes+                    , authenticationSecret = authSecretBytes+                    , salt = randSalt+                    , plainText = plainMessage64Encoded+                    , paddingLength = padLen+                    }+            eitherEncryptionOutput = webPushEncrypt encryptionInput+        encryptionOutput <- either (throw . toException . MessageEncryptionFailed) pure eitherEncryptionOutput+        let ecdhServerPublicKeyBytes = LB.toStrict . ecPublicKeyToBytes . ECDH.calculatePublic (ECC.getCurveByName ECC.SEC_p256r1) $ ecdhServerPrivateKey+            -- content-length is automtically added before making the http request+            authorizationHeader = LB.toStrict $ "WebPush " <> jwt+            cryptoKeyHeader = BS.concat [ "dh=", b64UrlNoPadding ecdhServerPublicKeyBytes+                                        , ";"+                                        , "p256ecdsa=", b64UrlNoPadding vapidPublicKeyBytestring+                                        ]+            postHeaders = [ ("TTL", C8.pack $ show $ pushNotification ^. pushExpireInSeconds)+                           , (hContentType, "application/octet-stream")+                           , (hAuthorization, authorizationHeader)+                           , ("Crypto-Key", cryptoKeyHeader)+                           , (hContentEncoding, "aesgcm")+                           , ("Encryption", "salt=" <> (b64UrlNoPadding randSalt))+                          ] -                    let authSecretBytes = B64.URL.decodeLenient $ TE.encodeUtf8 $ auth pushNotification-                        -- extract the 65 bytes of ECDH uncompressed public key received from browser in subscription-                        subscriptionPublicKeyBytes = B64.URL.decodeLenient $ TE.encodeUtf8 $ p256dh pushNotification-                        -- encode the message to a safe representation like base64URL before sending it to encryption algorithms-                        -- decode the message through service workers on browsers before trying to read the JSON-                        plainMessage64Encoded = B64.URL.Lazy.encode $ A.encode $ A.toJSON $ message pushNotification+            request = initReq { method = "POST"+                            , requestHeaders = postHeaders +++                                                    -- avoid duplicate headers+                                                    (filter (\(x, _) -> L.notElem x $ map fst postHeaders)+                                                            (requestHeaders initReq)+                                                    )+                                -- the body is encrypted message in raw bytes+                                -- without URL encoding+                            , requestBody = RequestBodyBS $ encryptedMessage encryptionOutput+                            }+        httpLbs request $ httpManager+    return $ either (Left . onError) (Right . (const ())) result -                        eitherEncryptionOutput = webPushEncrypt $ EncryptionInput { applicationServerPrivateKey = ecdhServerPrivateKey-                                                                                  , userAgentPublicKeyBytes = subscriptionPublicKeyBytes-                                                                                  , authenticationSecret = authSecretBytes-                                                                                  , salt = randSalt-                                                                                  , plainText = plainMessage64Encoded-                                                                                  , paddingLength = padLen-                                                                                  }-                    case eitherEncryptionOutput of-                        Left err -> return $ Left $ MessageEncryptionFailed err-                        Right encryptionOutput -> do+    where+        vapidPublicKeyBytestring = LB.toStrict . ecPublicKeyToBytes . ECDSA.public_q . ECDSA.toPublicKey $ vapidKeys+        onError :: SomeException -> PushNotificationError+        onError err+            | Just (x :: PushNotificationError) <- fromException err = x+            | Just (HttpExceptionRequest _ (StatusCodeException resp _)) <- fromException err = case statusCode (responseStatus resp) of+                -- when the endpoint is invalid, we need to remove it from database+                404 -> RecepientEndpointNotFound+                410 -> RecepientEndpointNotFound+                _ -> PushRequestFailed err+            | otherwise = PushRequestFailed err -                            let ecdhServerPublicKeyBytes = LB.toStrict $ ecPublicKeyToBytes $-                                                               ECDH.calculatePublic (ECC.getCurveByName ECC.SEC_p256r1) $-                                                                   ecdhServerPrivateKey-                            -- content-length is automtically added before making the http request-                            let postHeaders = let authorizationHeader = LB.toStrict $ "WebPush " <> jwt-                                                  cryptoKeyHeader = BS.concat [ "dh=", b64UrlNoPadding ecdhServerPublicKeyBytes-                                                                              , ";"-                                                                              , "p256ecdsa=", b64UrlNoPadding vapidPublicKeyBytestring-                                                                              ]-                                              in [ ("TTL", C8.pack $ show (60*60*(expireInHours pushNotification)))-                                                 , (hContentType, "application/octet-stream")-                                                 , (hAuthorization, authorizationHeader)-                                                 , ("Crypto-Key", cryptoKeyHeader)-                                                 , (hContentEncoding, "aesgcm")-                                                 , ("Encryption", "salt=" <> (b64UrlNoPadding randSalt))-                                                 ] -                                request = initReq { method = "POST"-                                                  , requestHeaders = postHeaders ++-                                                                         -- avoid duplicate headers-                                                                         (filter (\(x, _) -> L.notElem x $ map fst postHeaders)-                                                                                 (requestHeaders initReq)-                                                                         )-                                                    -- the body is encrypted message in raw bytes-                                                    -- without URL encoding-                                                  , requestBody = RequestBodyBS $ encryptedMessage encryptionOutput-                                                  }--                            eitherResp <- runCatchT $ liftIO $ httpLbs request httpManager-                            case eitherResp of-                                Left err@(SomeException _) -> case fromException err of-                                    Just (HttpExceptionRequest _ (StatusCodeException resp _))-                                        -- when the endpoint is invalid, we need to remove it from database-                                        |(statusCode (responseStatus resp) == 404) -> return $ Left RecepientEndpointNotFound-                                    _ -> return $ Left $ PushRequestFailed err-                                Right _ -> return $ Right ()+type PushEndpoint = T.Text+type PushP256dh = T.Text+type PushAuth = T.Text -    where+-- |Web push subscription and message details. Use 'mkPushNotification' to construct push notification.+data PushNotification msg = PushNotification {  _pnEndpoint :: PushEndpoint+                                              , _pnP256dh :: PushP256dh+                                              , _pnAuth :: PushAuth+                                              , _pnSenderEmail :: T.Text+                                              , _pnExpireInSeconds :: Int64+                                              , _pnMessage :: msg+                                              } -        vapidPublicKeyBytestring = LB.toStrict $ ecPublicKeyToBytes $-                                       ECDSA.public_q $ ECDSA.toPublicKey vapidKeys+pushEndpoint :: Lens' (PushNotification msg) PushEndpoint+pushEndpoint = lens _pnEndpoint (\d v -> d {_pnEndpoint = v}) +pushP256dh :: Lens' (PushNotification msg) PushP256dh+pushP256dh = lens _pnP256dh (\d v -> d {_pnP256dh = v}) +pushAuth :: Lens' (PushNotification msg) PushAuth+pushAuth = lens _pnAuth (\d v -> d {_pnAuth = v}) +pushSenderEmail :: Lens' (PushNotification msg) T.Text+pushSenderEmail = lens _pnSenderEmail (\d v -> d {_pnSenderEmail = v}) +pushExpireInSeconds :: Lens' (PushNotification msg) Int64+pushExpireInSeconds = lens _pnExpireInSeconds (\d v -> d {_pnExpireInSeconds = v}) +pushMessage :: (A.ToJSON msg) => Lens (PushNotification a) (PushNotification msg) a msg+pushMessage = lens _pnMessage (\d v -> d {_pnMessage = v}) --- |Web push subscription and message details.+-- |Constuct a push notification. ----- Get subscription details from front end using--- subscription.endpoint,--- subscription.toJSON().keys.p256dh and--- subscription.toJSON().keys.auth.--- Save subscription details to send messages to the endpoint in future.-data PushNotificationDetails = PushNotificationDetails { endpoint :: Text-                                                       , p256dh :: Text-                                                       , auth :: Text-                                                       , senderEmail :: Text-                                                       , expireInHours :: Int64-                                                       , message :: PushNotificationMessage-                                                       }+-- 'PushEndpoint', 'PushP256dh' and 'PushAuth' should be obtained from push subscription in client's browser.+-- Push message can be set through 'pushMessage'; text and json messages are usually supported by browsers.+-- 'pushSenderEmail' and 'pushExpireInSeconds' can be used to set additional details.+mkPushNotification :: PushEndpoint -> PushP256dh -> PushAuth -> PushNotification ()+mkPushNotification endpoint p256dh auth =+    PushNotification {+         _pnEndpoint = endpoint+       , _pnP256dh = p256dh+       , _pnAuth = auth+       , _pnSenderEmail = ""+       , _pnExpireInSeconds = 3600+       , _pnMessage = ()+    } +-- |Example payload structure for web-push.+-- Any datatype with JSON instance can also be used instead.+-- See 'mkPushNotification'.+data PushNotificationMessage = PushNotificationMessage+    { title :: T.Text+    , body :: T.Text+    , icon :: T.Text+    , url :: T.Text+    , tag :: T.Text+    } deriving (Eq, Show, Generic, A.ToJSON) --- |3 integers minimally representing a unique VAPID key pair.++-- |3 integers minimally representing a unique VAPID public-private key pair. data VAPIDKeysMinDetails = VAPIDKeysMinDetails { privateNumber :: Integer                                                , publicCoordX :: Integer                                                , publicCoordY :: Integer-                                               }+                                               } deriving (Show)  -- |'RecepientEndpointNotFound' comes up when the endpoint is no longer recognized by the push service. -- This may happen if the user has cancelled the push subscription, and hence deleted the endpoint.--- You may want to delete the endpoint from database in this case.+-- You may want to delete the endpoint from database in this case, or if 'EndpointParseFailed'. data PushNotificationError = EndpointParseFailed SomeException-                           | JWTGenerationFailed JOSE.Error.Error                            | MessageEncryptionFailed CryptoError                            | RecepientEndpointNotFound                            | PushRequestFailed SomeException+                            deriving (Show, Exception)
src/Web/WebPush/Internal.hs view
@@ -6,30 +6,20 @@ import Data.ByteString                                         (ByteString) import qualified Data.ByteString                 as BS import qualified Data.ByteString.Lazy            as LB-import Data.Monoid                                             ((<>)) import Data.Text                                               (Text)-import qualified Data.HashMap.Strict             as HM--+import Data.Time.Format                                        (formatTime, defaultTimeLocale)+import Data.Time                                               (getCurrentTime, addUTCTime) import qualified Crypto.PubKey.ECC.Types         as ECC import qualified Crypto.PubKey.ECC.ECDSA         as ECDSA-import Crypto.Hash.Algorithms                                  (SHA256(..)) import qualified Crypto.PubKey.ECC.DH            as ECDH import qualified Crypto.MAC.HMAC                 as HMAC-import Crypto.Cipher.AES                                       (AES128) import qualified Crypto.Cipher.Types             as Cipher+import Crypto.Hash.Algorithms                                  (SHA256(..))+import Crypto.Cipher.AES                                       (AES128) import Crypto.Error                                            (CryptoFailable(CryptoPassed,CryptoFailed), CryptoError)--import Crypto.JWT                                              (createJWSJWT, ClaimsSet(..))-import qualified Crypto.JWT                      as JWT-import qualified Crypto.JOSE.JWK                 as JWK-import Crypto.JOSE.JWS                                         (JWSHeader(..), Alg(ES256))-import qualified Crypto.JOSE.Header              as JOSE.Header-import qualified Crypto.JOSE.Types               as JOSE-import qualified Crypto.JOSE.Compact             as JOSE.Compact-import qualified Crypto.JOSE.Error               as JOSE.Error+import Network.HTTP.Client                                     (Request, host, secure) -import Data.Aeson                                              (ToJSON, toJSON, (.=))+import Data.Aeson                                              ((.=)) import qualified Data.Aeson                      as A import qualified Data.ByteString.Base64.URL      as B64.URL @@ -39,112 +29,51 @@ import qualified Data.ByteArray                  as ByteArray  import Control.Monad.IO.Class                                  (MonadIO, liftIO)-import Control.Monad.Except                                    (runExceptT)--+import qualified Data.Text.Encoding              as TE+import qualified Data.Text.Encoding.Error        as TE+import qualified Data.Text                       as T  type VAPIDKeys = ECDSA.KeyPair -data VAPIDClaims = VAPIDClaims { vapidAud :: JWT.Audience-                               , vapidSub :: JWT.StringOrURI-                               , vapidExp :: JWT.NumericDate-                               }--- JSON Web Token for VAPID-webPushJWT :: MonadIO m => VAPIDKeys -> VAPIDClaims -> m (Either (JOSE.Error.Error) LB.ByteString)-webPushJWT vapidKeys vapidClaims = do-    let ECC.Point publicKeyX publicKeyY = ECDSA.public_q $ ECDSA.toPublicKey vapidKeys-        privateKeyNumber = ECDSA.private_d $ ECDSA.toPrivateKey vapidKeys--    liftIO $ runExceptT $ do-        jwtData <- createJWSJWT ( JWK.fromKeyMaterial $ JWK.ECKeyMaterial $-                                     JWK.ECKeyParameters { JWK.ecKty = JWK.EC-                                                         , JWK.ecCrv = JWK.P_256-                                                         , JWK.ecX = JOSE.SizedBase64Integer 32 $ publicKeyX-                                                         , JWK.ecY = JOSE.SizedBase64Integer 32 $ publicKeyY-                                                         , JWK.ecD = Just $ JOSE.SizedBase64Integer 32 $ privateKeyNumber-                                                         }-                                )-                                ( JWSHeader { _jwsHeaderAlg = JOSE.Header.HeaderParam JOSE.Header.Protected ES256-                                            , _jwsHeaderJku = Nothing-                                            , _jwsHeaderJwk = Nothing-                                            , _jwsHeaderKid = Nothing-                                            , _jwsHeaderX5u = Nothing-                                            , _jwsHeaderX5c = Nothing-                                            , _jwsHeaderX5t = Nothing-                                            , _jwsHeaderX5tS256 = Nothing-                                            , _jwsHeaderTyp = Just (JOSE.Header.HeaderParam JOSE.Header.Protected "JWT")-                                            , _jwsHeaderCty = Nothing-                                            , _jwsHeaderCrit = Nothing-                                            }-                                )-                                ( ClaimsSet { _claimIss = Nothing-                                            , _claimSub = Just $ vapidSub $ vapidClaims-                                            , _claimAud = Just $ vapidAud $ vapidClaims-                                            , _claimExp = Just $ vapidExp $ vapidClaims-                                            , _claimNbf = Nothing-                                            , _claimIat = Nothing-                                            , _claimJti = Nothing-                                            , _unregisteredClaims = HM.empty-                                            }-                                )--        JOSE.Compact.encodeCompact $ jwtData--            -----------------------------            {--            -- Manual implementation without using the JWT libraries.-            -- This works as well.-            -- Kept here mainly as process explanation.--            -- JWT base 64 encoding is without padding-            let messageForJWTSignature = let encodedJWTPayload = b64UrlNoPadding $ LB.toStrict $ A.encode $ A.object $-                                                 [ "aud" .= (TE.decodeUtf8With TE.lenientDecode $-                                                                (if secure initReq then "https://" else "http://") ++ (host initReq)-                                                            )-                                                 -- jwt expiration time-                                                 , "exp" .= (formatTime defaultTimeLocale "%s" $ addUTCTime 3000 time)-                                                 , "sub" .= ("mailto: " ++ (senderEmail pushNotification))-                                                 ]--                                             encodedJWTHeader = b64UrlNoPadding $ LB.toStrict $ A.encode $ A.object $-                                                 [ "typ" .= ("JWT" :: Text), "alg" .= ("ES256" :: Text) ]+----------------------------+-- Manual implementation without using the JWT libraries.+-- Not using jose library. Check the below link for reason:+-- https://github.com/sarthakbagaria/web-push/pull/1#issuecomment-471254455+webPushJWT :: MonadIO m => VAPIDKeys -> Request -> T.Text -> m LB.ByteString+webPushJWT vapidKeys initReq senderEmail = do+    -- JWT base 64 encoding is without padding+    time <- liftIO getCurrentTime+    let messageForJWTSignature =+            let proto = if secure initReq then "https://" else "http://"+                encodedJWTPayload = b64UrlNoPadding . LB.toStrict . A.encode . A.object $+                    [ "aud" .= (TE.decodeUtf8With TE.lenientDecode $ proto <> (host initReq))+                    , "exp" .= (formatTime defaultTimeLocale "%s" $ addUTCTime 3000 time) -- jwt expiration time+                    , "sub" .= ("mailto: " <> senderEmail)+                    ] -                                         in encodedJWTHeader <> "." <> encodedJWTPayload+                encodedJWTHeader = b64UrlNoPadding . LB.toStrict . A.encode . A.object $+                    [ "typ" .= ("JWT" :: Text)+                    , "alg" .= ("ES256" :: Text)+                    ] -            -- JWT only accepts SHA256 hash with ECDSA for ES256 signed token-            encodedJWTSignature <- do-                -- ECDSA signing vulnerable to timing attacks-                ECDSA.Signature signR signS <- liftIO $ ECDSA.sign (ECDSA.toPrivateKey vapidKeys)-                                                                   SHA256-                                                                   messageForJWTSignature+            in encodedJWTHeader <> "." <> encodedJWTPayload -                -- 32 bytes of R followed by 32 bytes of S-                return $ b64UrlNoPadding $ LB.toStrict $-                             (Binary.encode $ int32Bytes signR) <>-                             (Binary.encode $ int32Bytes signS)+    -- JWT only accepts SHA256 hash with ECDSA for ES256 signed token+    encodedJWTSignature <- do+        -- ECDSA signing vulnerable to timing attacks+        ECDSA.Signature signR signS <- liftIO $ ECDSA.sign (ECDSA.toPrivateKey vapidKeys)+                                                            SHA256+                                                            messageForJWTSignature -            return $ Right $ messageForJWTSignature <> "." <> encodedJWTSignature-            -}-            ----------------------------+        -- 32 bytes of R followed by 32 bytes of S+        return $ b64UrlNoPadding $ LB.toStrict $+                        (Binary.encode $ int32Bytes signR) <>+                        (Binary.encode $ int32Bytes signS) +    let res = messageForJWTSignature <> "." <> encodedJWTSignature+    pure . LB.fromStrict $ res  -data PushNotificationMessage = PushNotificationMessage { title :: Text-                                                       , body :: Text-                                                       , icon :: Text-                                                       , url :: Text-                                                       , tag :: Text-                                                       }--instance ToJSON PushNotificationMessage where-   toJSON PushNotificationMessage {..} = A.object-       [ "title" .= title-       , "body" .= body-       , "icon" .= icon-       , "url" .= url-       , "tag" .= tag-       ]- -- All inputs are in raw bytes with no encoding -- except for the plaintext for which raw bytes are the Base 64 encoded bytes data WebPushEncryptionInput = EncryptionInput { applicationServerPrivateKey :: ECDH.PrivateNumber@@ -177,7 +106,6 @@         userAgentPublicKey = ecBytesToPublicKey userAgentPublicKeyBytes         sharedECDHSecret = ECDH.getShared (ECC.getCurveByName ECC.SEC_p256r1) applicationServerPrivateKey userAgentPublicKey -         -- HMAC key derivation (HKDF, here expanded into HMAC steps as specified in web push encryption spec)         pseudoRandomKeyCombine = HMAC.hmac authenticationSecret sharedECDHSecret :: HMAC.HMAC SHA256         authInfo = "Content-Encoding: auth" <> "\x00" :: ByteString@@ -194,7 +122,6 @@         nonceContext = "Content-Encoding: nonce" <> "\x00" <> context         nonce = BS.pack $ take 12 $ ByteArray.unpack (HMAC.hmac pseudoRandomKeyEncryption (nonceContext <> "\x01") :: HMAC.HMAC SHA256) -         -- HMAC a doesn't have Show instance needed for test suite         -- so we extract the bytes and store that in WebPushEncryptionOutput         inputKeyingMaterialBytes = ByteArray.convert inputKeyingMaterial@@ -231,10 +158,8 @@                         in Right $ EncryptionOutput {..}  - -- Conversions among integers and bytes -- The bytes are in network/big endian order.-  {-     -- DON'T use DER encoding to extract integer bytes     -- if a 32 byte number can be written in less bytes with leading zeros,
web-push.cabal view
@@ -1,7 +1,10 @@ name:                web-push-version:             0.1.2.0+version:             0.3 synopsis:            Send messages using Web Push protocol.-description:         Please see README.md+description:+    Web Push is a simple protocol for delivery of real-time events+    to user agents using HTTP/2 server push. This can be used to send+    notifications to browsers using the Push API. homepage:            https://github.com/sarthakbagaria/web-push#readme license:             MIT license-file:        LICENSE@@ -24,17 +27,15 @@                      , bytestring                    >= 0.9        && < 0.11                      , base64-bytestring             >= 1.0.0.1    && < 1.1                      , text                          >= 0.11       && < 2.0-                     , cryptonite                    >= 0.21       && < 0.22+                     , cryptonite                    >= 0.24                      , binary                        >= 0.7.5      && < 0.9-                     , memory                        >= 0.14.5     && < 0.15+                     , memory                        >= 0.14.5     && < 0.16                      , random                        >= 1.1        && < 1.2-                     , http-client                   >= 0.5.7      && < 0.6+                     , http-client                   >= 0.5.7      && < 0.7                      , http-types                    >= 0.8.6      && < 1.0-                     , exceptions                    >= 0.8.3      && < 0.9                      , transformers                  >= 0.5.2.0    && < 0.6-                     , mtl                           >= 2.2.1      && < 2.3-                     , jose                          >= 0.5.0.4    && < 0.6-                     , unordered-containers+                     , lens                          >= 4.15.1+                     , safe-exceptions               >= 0.1.7      && < 0.2    default-language:    Haskell2010 @@ -46,7 +47,7 @@   ghc-options:         -Wall   build-depends:       base                      , web-push-                     , hspec                         >= 2.4.3      && < 2.5+                     , hspec                         >= 2.4.3      && < 2.8                      , bytestring                    >= 0.9        && < 0.11                      , base64-bytestring             >= 1.0.0.1    && < 1.1                      , binary                        >= 0.7.5      && < 0.9