Skip to content

Commit e90c15b

Browse files
committed
Move generic push functions to Push.hs
1 parent a2d777b commit e90c15b

File tree

2 files changed

+82
-58
lines changed

2 files changed

+82
-58
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{-# LANGUAGE DeriveAnyClass #-}
2+
{-# LANGUAGE DuplicateRecordFields #-}
3+
{-# LANGUAGE LambdaCase #-}
4+
{-# LANGUAGE NamedFieldPuns #-}
5+
{-# LANGUAGE OverloadedStrings #-}
6+
{-# LANGUAGE TemplateHaskell #-}
7+
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
8+
9+
{-# HLINT ignore "Use newtype instead of data" #-}
10+
11+
module Simplex.Messaging.Notifications.Server.Push where
12+
13+
import Crypto.Hash.Algorithms (SHA256 (..))
14+
import qualified Crypto.PubKey.ECC.ECDSA as EC
15+
import qualified Crypto.PubKey.ECC.Types as ECT
16+
import qualified Crypto.Store.PKCS8 as PK
17+
import Data.ASN1.BinaryEncoding (DER (..))
18+
import Data.ASN1.Encoding
19+
import Data.ASN1.Types
20+
import Data.Aeson (ToJSON)
21+
import qualified Data.Aeson as J
22+
import qualified Data.Aeson.TH as JQ
23+
import qualified Data.ByteString.Base64.URL as U
24+
import Data.ByteString.Char8 (ByteString)
25+
import qualified Data.ByteString.Lazy.Char8 as LB
26+
import Data.Int (Int64)
27+
import Data.List.NonEmpty (NonEmpty (..))
28+
import Data.Text (Text)
29+
import Data.Time.Clock.System
30+
import qualified Data.X509 as X
31+
import Simplex.Messaging.Notifications.Protocol
32+
import Simplex.Messaging.Parsers (defaultJSON)
33+
34+
data JWTHeader = JWTHeader
35+
{ alg :: Text, -- key algorithm, ES256 for APNS
36+
kid :: Text -- key ID
37+
}
38+
deriving (Show)
39+
40+
data JWTClaims = JWTClaims
41+
{ iss :: Text, -- issuer, team ID for APNS
42+
iat :: Int64 -- issue time, seconds from epoch
43+
}
44+
deriving (Show)
45+
46+
data JWTToken = JWTToken JWTHeader JWTClaims
47+
deriving (Show)
48+
49+
mkJWTToken :: JWTHeader -> Text -> IO JWTToken
50+
mkJWTToken hdr iss = do
51+
iat <- systemSeconds <$> getSystemTime
52+
pure $ JWTToken hdr JWTClaims {iss, iat}
53+
54+
type SignedJWTToken = ByteString
55+
56+
$(JQ.deriveToJSON defaultJSON ''JWTHeader)
57+
58+
$(JQ.deriveToJSON defaultJSON ''JWTClaims)
59+
60+
signedJWTToken :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken
61+
signedJWTToken pk (JWTToken hdr claims) = do
62+
let hc = jwtEncode hdr <> "." <> jwtEncode claims
63+
sig <- EC.sign pk SHA256 hc
64+
pure $ hc <> "." <> serialize sig
65+
where
66+
jwtEncode :: ToJSON a => a -> ByteString
67+
jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode
68+
serialize sig = U.encodeUnpadded $ encodeASN1' DER [Start Sequence, IntVal (EC.sign_r sig), IntVal (EC.sign_s sig), End Sequence]
69+
70+
readECPrivateKey :: FilePath -> IO EC.PrivateKey
71+
readECPrivateKey f = do
72+
-- this pattern match is specific to APNS key type, it may need to be extended for other push providers
73+
[PK.Unprotected (X.PrivKeyEC X.PrivKeyEC_Named {privkeyEC_name, privkeyEC_priv})] <- PK.readKeyFile f
74+
pure EC.PrivateKey {private_curve = ECT.getCurveByName privkeyEC_name, private_d = privkeyEC_priv}
75+
76+
data PushNotification
77+
= PNVerification NtfRegCode
78+
| PNMessage (NonEmpty PNMessageData)
79+
| -- | PNAlert Text
80+
PNCheckMessages
81+
deriving (Show)

src/Simplex/Messaging/Notifications/Server/Push/APNS.hs

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,8 @@ import Control.Monad
1616
import Control.Monad.Except
1717
import Control.Monad.IO.Class
1818
import Control.Monad.Trans.Except
19-
import Crypto.Hash.Algorithms (SHA256 (..))
2019
import qualified Crypto.PubKey.ECC.ECDSA as EC
21-
import qualified Crypto.PubKey.ECC.Types as ECT
2220
import Crypto.Random (ChaChaDRG)
23-
import qualified Crypto.Store.PKCS8 as PK
24-
import Data.ASN1.BinaryEncoding (DER (..))
25-
import Data.ASN1.Encoding
26-
import Data.ASN1.Types
2721
import Data.Aeson (ToJSON, (.=))
2822
import qualified Data.Aeson as J
2923
import qualified Data.Aeson.Encoding as JE
@@ -32,18 +26,15 @@ import Data.Bifunctor (first)
3226
import qualified Data.ByteString.Base64.URL as U
3327
import Data.ByteString.Builder (lazyByteString)
3428
import Data.ByteString.Char8 (ByteString)
35-
import qualified Data.ByteString.Lazy.Char8 as LB
3629
import qualified Data.CaseInsensitive as CI
3730
import Data.Int (Int64)
3831
import Data.List (find)
39-
import Data.List.NonEmpty (NonEmpty (..))
4032
import Data.Map.Strict (Map)
4133
import Data.Maybe (isNothing)
4234
import Data.Text (Text)
4335
import qualified Data.Text as T
4436
import Data.Text.Encoding (encodeUtf8)
4537
import Data.Time.Clock.System
46-
import qualified Data.X509 as X
4738
import qualified Data.X509.CertificateStore as XS
4839
import Network.HPACK.Token as HT
4940
import Network.HTTP.Types (Status)
@@ -53,6 +44,7 @@ import qualified Network.HTTP2.Client as H
5344
import Network.Socket (HostName, ServiceName)
5445
import qualified Simplex.Messaging.Crypto as C
5546
import Simplex.Messaging.Notifications.Protocol
47+
import Simplex.Messaging.Notifications.Server.Push
5648
import Simplex.Messaging.Notifications.Server.Push.APNS.Internal
5749
import Simplex.Messaging.Notifications.Server.Store.Types (NtfTknRec (..))
5850
import Simplex.Messaging.Parsers (defaultJSON)
@@ -62,55 +54,6 @@ import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
6254
import System.Environment (getEnv)
6355
import UnliftIO.STM
6456

65-
data JWTHeader = JWTHeader
66-
{ alg :: Text, -- key algorithm, ES256 for APNS
67-
kid :: Text -- key ID
68-
}
69-
deriving (Show)
70-
71-
data JWTClaims = JWTClaims
72-
{ iss :: Text, -- issuer, team ID for APNS
73-
iat :: Int64 -- issue time, seconds from epoch
74-
}
75-
deriving (Show)
76-
77-
data JWTToken = JWTToken JWTHeader JWTClaims
78-
deriving (Show)
79-
80-
mkJWTToken :: JWTHeader -> Text -> IO JWTToken
81-
mkJWTToken hdr iss = do
82-
iat <- systemSeconds <$> getSystemTime
83-
pure $ JWTToken hdr JWTClaims {iss, iat}
84-
85-
type SignedJWTToken = ByteString
86-
87-
$(JQ.deriveToJSON defaultJSON ''JWTHeader)
88-
89-
$(JQ.deriveToJSON defaultJSON ''JWTClaims)
90-
91-
signedJWTToken :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken
92-
signedJWTToken pk (JWTToken hdr claims) = do
93-
let hc = jwtEncode hdr <> "." <> jwtEncode claims
94-
sig <- EC.sign pk SHA256 hc
95-
pure $ hc <> "." <> serialize sig
96-
where
97-
jwtEncode :: ToJSON a => a -> ByteString
98-
jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode
99-
serialize sig = U.encodeUnpadded $ encodeASN1' DER [Start Sequence, IntVal (EC.sign_r sig), IntVal (EC.sign_s sig), End Sequence]
100-
101-
readECPrivateKey :: FilePath -> IO EC.PrivateKey
102-
readECPrivateKey f = do
103-
-- this pattern match is specific to APNS key type, it may need to be extended for other push providers
104-
[PK.Unprotected (X.PrivKeyEC X.PrivKeyEC_Named {privkeyEC_name, privkeyEC_priv})] <- PK.readKeyFile f
105-
pure EC.PrivateKey {private_curve = ECT.getCurveByName privkeyEC_name, private_d = privkeyEC_priv}
106-
107-
data PushNotification
108-
= PNVerification NtfRegCode
109-
| PNMessage (NonEmpty PNMessageData)
110-
| -- | PNAlert Text
111-
PNCheckMessages
112-
deriving (Show)
113-
11457
data APNSNotification = APNSNotification {aps :: APNSNotificationBody, notificationData :: Maybe J.Value}
11558
deriving (Show)
11659

0 commit comments

Comments
 (0)