-
-
Notifications
You must be signed in to change notification settings - Fork 81
Webpush requests #1613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Webpush requests #1613
Changes from all commits
e90c15b
c08c379
e7e7c9b
64269c2
a24024c
2205a1f
1295b28
720fb40
9d30af4
4a39c4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -278,7 +278,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), Ratc | |
import qualified Simplex.Messaging.Crypto.Ratchet as CR | ||
import Simplex.Messaging.Encoding | ||
import Simplex.Messaging.Encoding.String | ||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfSubscriptionId, NtfTknStatus (..), NtfTokenId, SMPQueueNtf (..)) | ||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfSubscriptionId, NtfTknStatus (..), NtfTokenId, SMPQueueNtf (..), deviceTokenFields, deviceToken') | ||
import Simplex.Messaging.Notifications.Types | ||
import Simplex.Messaging.Parsers (parseAll) | ||
import Simplex.Messaging.Protocol | ||
|
@@ -1382,7 +1382,8 @@ deleteCommand db cmdId = | |
DB.execute db "DELETE FROM commands WHERE command_id = ?" (Only cmdId) | ||
|
||
createNtfToken :: DB.Connection -> NtfToken -> IO () | ||
createNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer = srv@ProtocolServer {host, port}, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhKeys = (ntfDhPubKey, ntfDhPrivKey), ntfDhSecret, ntfTknStatus, ntfTknAction, ntfMode} = do | ||
createNtfToken db NtfToken {deviceToken, ntfServer = srv@ProtocolServer {host, port}, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhKeys = (ntfDhPubKey, ntfDhPrivKey), ntfDhSecret, ntfTknStatus, ntfTknAction, ntfMode} = do | ||
let (provider, token) = deviceTokenFields deviceToken | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we most likely need to add fields for webpush tokens to the table. |
||
upsertNtfServer_ db srv | ||
DB.execute | ||
db | ||
|
@@ -1409,10 +1410,12 @@ getSavedNtfToken db = do | |
let ntfServer = NtfServer host port keyHash | ||
ntfDhKeys = (ntfDhPubKey, ntfDhPrivKey) | ||
ntfMode = fromMaybe NMPeriodic ntfMode_ | ||
in NtfToken {deviceToken = DeviceToken provider dt, ntfServer, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhKeys, ntfDhSecret, ntfTknStatus, ntfTknAction, ntfMode} | ||
deviceToken = deviceToken' provider dt | ||
in NtfToken {deviceToken, ntfServer, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhKeys, ntfDhSecret, ntfTknStatus, ntfTknAction, ntfMode} | ||
|
||
updateNtfTokenRegistration :: DB.Connection -> NtfToken -> NtfTokenId -> C.DhSecretX25519 -> IO () | ||
updateNtfTokenRegistration db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} tknId ntfDhSecret = do | ||
updateNtfTokenRegistration db NtfToken {deviceToken, ntfServer = ProtocolServer {host, port}} tknId ntfDhSecret = do | ||
let (provider, token) = deviceTokenFields deviceToken | ||
updatedAt <- getCurrentTime | ||
DB.execute | ||
db | ||
|
@@ -1424,8 +1427,10 @@ updateNtfTokenRegistration db NtfToken {deviceToken = DeviceToken provider token | |
(tknId, ntfDhSecret, NTRegistered, Nothing :: Maybe NtfTknAction, updatedAt, provider, token, host, port) | ||
|
||
updateDeviceToken :: DB.Connection -> NtfToken -> DeviceToken -> IO () | ||
updateDeviceToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} (DeviceToken toProvider toToken) = do | ||
updateDeviceToken db NtfToken {deviceToken, ntfServer = ProtocolServer {host, port}} toDt = do | ||
let (provider, token) = deviceTokenFields deviceToken | ||
updatedAt <- getCurrentTime | ||
let (toProvider, toToken) = deviceTokenFields toDt | ||
DB.execute | ||
db | ||
[sql| | ||
|
@@ -1436,7 +1441,8 @@ updateDeviceToken db NtfToken {deviceToken = DeviceToken provider token, ntfServ | |
(toProvider, toToken, NTRegistered, Nothing :: Maybe NtfTknAction, updatedAt, provider, token, host, port) | ||
|
||
updateNtfMode :: DB.Connection -> NtfToken -> NotificationsMode -> IO () | ||
updateNtfMode db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} ntfMode = do | ||
updateNtfMode db NtfToken {deviceToken, ntfServer = ProtocolServer {host, port}} ntfMode = do | ||
let (provider, token) = deviceTokenFields deviceToken | ||
updatedAt <- getCurrentTime | ||
DB.execute | ||
db | ||
|
@@ -1448,7 +1454,8 @@ updateNtfMode db NtfToken {deviceToken = DeviceToken provider token, ntfServer = | |
(ntfMode, updatedAt, provider, token, host, port) | ||
|
||
updateNtfToken :: DB.Connection -> NtfToken -> NtfTknStatus -> Maybe NtfTknAction -> IO () | ||
updateNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} tknStatus tknAction = do | ||
updateNtfToken db NtfToken {deviceToken, ntfServer = ProtocolServer {host, port}} tknStatus tknAction = do | ||
let (provider, token) = deviceTokenFields deviceToken | ||
updatedAt <- getCurrentTime | ||
DB.execute | ||
db | ||
|
@@ -1460,7 +1467,8 @@ updateNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer | |
(tknStatus, tknAction, updatedAt, provider, token, host, port) | ||
|
||
removeNtfToken :: DB.Connection -> NtfToken -> IO () | ||
removeNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} = | ||
removeNtfToken db NtfToken {deviceToken, ntfServer = ProtocolServer {host, port}} = do | ||
let (provider, token) = deviceTokenFields deviceToken | ||
DB.execute | ||
db | ||
[sql| | ||
|
@@ -1785,7 +1793,8 @@ getActiveNtfToken db = | |
let ntfServer = NtfServer host port keyHash | ||
ntfDhKeys = (ntfDhPubKey, ntfDhPrivKey) | ||
ntfMode = fromMaybe NMPeriodic ntfMode_ | ||
in NtfToken {deviceToken = DeviceToken provider dt, ntfServer, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhKeys, ntfDhSecret, ntfTknStatus, ntfTknAction, ntfMode} | ||
deviceToken = deviceToken' provider dt | ||
in NtfToken {deviceToken, ntfServer, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhKeys, ntfDhSecret, ntfTknStatus, ntfTknAction, ntfMode} | ||
|
||
getNtfRcvQueue :: DB.Connection -> SMPQueueNtf -> IO (Either StoreError (ConnId, Int64, RcvNtfDhSecret, Maybe UTCTime)) | ||
getNtfRcvQueue db SMPQueueNtf {smpServer = (SMPServer host port _), notifierId} = | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -127,6 +127,7 @@ module Simplex.Messaging.Crypto | |||||
encryptAEAD, | ||||||
decryptAEAD, | ||||||
encryptAESNoPad, | ||||||
encryptAES128NoPad, | ||||||
decryptAESNoPad, | ||||||
authTagSize, | ||||||
randomAesKey, | ||||||
|
@@ -209,7 +210,7 @@ import Control.Exception (Exception) | |||||
import Control.Monad | ||||||
import Control.Monad.Except | ||||||
import Control.Monad.Trans.Except | ||||||
import Crypto.Cipher.AES (AES256) | ||||||
import Crypto.Cipher.AES (AES256, AES128) | ||||||
import qualified Crypto.Cipher.Types as AES | ||||||
import qualified Crypto.Cipher.XSalsa as XSalsa | ||||||
import qualified Crypto.Error as CE | ||||||
|
@@ -895,6 +896,8 @@ data CryptoError | |||||
CERatchetEarlierMessage Word32 | ||||||
| -- | duplicate message number | ||||||
CERatchetDuplicateMessage | ||||||
| -- | unable to decode ecc key | ||||||
CryptoInvalidECCKey CE.CryptoError | ||||||
Comment on lines
+899
to
+900
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This error is not necessary as it would be a parsing error. |
||||||
deriving (Eq, Show, Exception) | ||||||
|
||||||
aesKeySize :: Int | ||||||
|
@@ -1021,11 +1024,22 @@ encryptAESNoPad :: Key -> GCMIV -> ByteString -> ExceptT CryptoError IO (AuthTag | |||||
encryptAESNoPad key iv = encryptAEADNoPad key iv "" | ||||||
{-# INLINE encryptAESNoPad #-} | ||||||
|
||||||
-- Used to encrypt WebPush notifications | ||||||
-- This function requires 12 bytes IV, it does not transform IV. | ||||||
encryptAES128NoPad :: Key -> GCMIV -> ByteString -> ExceptT CryptoError IO (AuthTag, ByteString) | ||||||
encryptAES128NoPad key iv = encryptAEAD128NoPad key iv "" | ||||||
{-# INLINE encryptAES128NoPad #-} | ||||||
|
||||||
encryptAEADNoPad :: Key -> GCMIV -> ByteString -> ByteString -> ExceptT CryptoError IO (AuthTag, ByteString) | ||||||
encryptAEADNoPad aesKey ivBytes ad msg = do | ||||||
aead <- initAEADGCM aesKey ivBytes | ||||||
pure . first AuthTag $ AES.aeadSimpleEncrypt aead ad msg authTagSize | ||||||
|
||||||
encryptAEAD128NoPad :: Key -> GCMIV -> ByteString -> ByteString -> ExceptT CryptoError IO (AuthTag, ByteString) | ||||||
encryptAEAD128NoPad aesKey ivBytes ad msg = do | ||||||
aead <- initAEAD128GCM aesKey ivBytes | ||||||
epoberezkin marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
pure . first AuthTag $ AES.aeadSimpleEncrypt aead ad msg authTagSize | ||||||
|
||||||
-- | AEAD-GCM decryption with associated data. | ||||||
-- | ||||||
-- Used as part of double ratchet encryption. | ||||||
|
@@ -1125,6 +1139,12 @@ initAEADGCM (Key aesKey) (GCMIV ivBytes) = cryptoFailable $ do | |||||
cipher <- AES.cipherInit aesKey | ||||||
AES.aeadInit AES.AEAD_GCM cipher ivBytes | ||||||
|
||||||
-- this function requires 12 bytes IV, it does not transforms IV. | ||||||
initAEAD128GCM :: Key -> GCMIV -> ExceptT CryptoError IO (AES.AEAD AES128) | ||||||
initAEAD128GCM (Key aesKey) (GCMIV ivBytes) = cryptoFailable $ do | ||||||
cipher <- AES.cipherInit aesKey | ||||||
AES.aeadInit AES.AEAD_GCM cipher ivBytes | ||||||
|
||||||
-- | Random AES256 key. | ||||||
randomAesKey :: TVar ChaChaDRG -> STM Key | ||||||
randomAesKey = fmap Key . randomBytes aesKeySize | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,7 @@ import Simplex.Messaging.Encoding.String | |
import Simplex.Messaging.Notifications.Transport (NTFVersion, invalidReasonNTFVersion, ntfClientHandshake) | ||
import Simplex.Messaging.Protocol hiding (Command (..), CommandTag (..)) | ||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>)) | ||
import Control.Monad (when) | ||
|
||
data NtfEntity = Token | Subscription | ||
deriving (Show) | ||
|
@@ -377,6 +378,7 @@ data PushProvider | |
| PPApnsProd -- production environment, including TestFlight | ||
| PPApnsTest -- used for tests, to use APNS mock server | ||
| PPApnsNull -- used to test servers from the client - does not communicate with APNS | ||
| PPWebPush -- used for webpush (FCM, UnifiedPush, potentially desktop) | ||
deriving (Eq, Ord, Show) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Proposed type for push provider (it's needed as a key for the connected client), but it probably is not needed during parsing of DeviceToken:
|
||
|
||
instance Encoding PushProvider where | ||
|
@@ -385,12 +387,14 @@ instance Encoding PushProvider where | |
PPApnsProd -> "AP" | ||
PPApnsTest -> "AT" | ||
PPApnsNull -> "AN" | ||
PPWebPush -> "WP" | ||
smpP = | ||
A.take 2 >>= \case | ||
"AD" -> pure PPApnsDev | ||
"AP" -> pure PPApnsProd | ||
"AT" -> pure PPApnsTest | ||
"AN" -> pure PPApnsNull | ||
"WP" -> pure PPWebPush | ||
_ -> fail "bad PushProvider" | ||
|
||
instance StrEncoding PushProvider where | ||
|
@@ -399,44 +403,116 @@ instance StrEncoding PushProvider where | |
PPApnsProd -> "apns_prod" | ||
PPApnsTest -> "apns_test" | ||
PPApnsNull -> "apns_null" | ||
PPWebPush -> "webpush" | ||
strP = | ||
A.takeTill (== ' ') >>= \case | ||
"apns_dev" -> pure PPApnsDev | ||
"apns_prod" -> pure PPApnsProd | ||
"apns_test" -> pure PPApnsTest | ||
"apns_null" -> pure PPApnsNull | ||
"webpush" -> pure PPWebPush | ||
_ -> fail "bad PushProvider" | ||
|
||
instance FromField PushProvider where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 | ||
|
||
instance ToField PushProvider where toField = toField . decodeLatin1 . strEncode | ||
|
||
data DeviceToken = DeviceToken PushProvider ByteString | ||
data WPEndpoint = WPEndpoint { endpoint::ByteString, auth::ByteString, p256dh::ByteString } | ||
deriving (Eq, Ord, Show) | ||
|
||
instance Encoding WPEndpoint where | ||
smpEncode WPEndpoint { endpoint, auth, p256dh } = smpEncode (endpoint, auth, p256dh) | ||
smpP = do | ||
endpoint <- smpP | ||
auth <- smpP | ||
p256dh <- smpP | ||
pure WPEndpoint { endpoint, auth, p256dh } | ||
|
||
instance StrEncoding WPEndpoint where | ||
strEncode WPEndpoint { endpoint, auth, p256dh } = endpoint <> " " <> strEncode auth <> " " <> strEncode p256dh | ||
strP = do | ||
endpoint <- A.takeWhile (/= ' ') | ||
_ <- A.char ' ' | ||
(auth, p256dh) <- strP | ||
-- auth is a 16 bytes long random key | ||
when (B.length auth /= 16) $ fail "Invalid auth key length" | ||
-- p256dh is a public key on the P-256 curve, encoded in uncompressed format | ||
-- 0x04 + the 2 points = 65 bytes | ||
when (B.length p256dh /= 65) $ fail "Invalid p256dh key length" | ||
when (B.take 1 p256dh /= "\x04") $ fail "Invalid p256dh key, doesn't start with 0x04" | ||
pure WPEndpoint { endpoint, auth, p256dh } | ||
|
||
instance ToJSON WPEndpoint where | ||
toEncoding WPEndpoint { endpoint, auth, p256dh } = J.pairs $ "endpoint" .= decodeLatin1 endpoint <> "auth" .= decodeLatin1 (strEncode auth) <> "p256dh" .= decodeLatin1 (strEncode p256dh) | ||
toJSON WPEndpoint { endpoint, auth, p256dh } = J.object ["endpoint" .= decodeLatin1 endpoint, "auth" .= decodeLatin1 (strEncode auth), "p256dh" .= decodeLatin1 (strEncode p256dh) ] | ||
|
||
instance FromJSON WPEndpoint where | ||
parseJSON = J.withObject "WPEndpoint" $ \o -> do | ||
endpoint <- encodeUtf8 <$> o .: "endpoint" | ||
auth <- strDecode . encodeUtf8 <$?> o .: "auth" | ||
p256dh <- strDecode . encodeUtf8 <$?> o .: "p256dh" | ||
pure WPEndpoint { endpoint, auth, p256dh } | ||
|
||
data DeviceToken | ||
= APNSDeviceToken PushProvider ByteString | ||
| WPDeviceToken WPEndpoint | ||
Comment on lines
+456
to
+458
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Proposed type for DeviceToken is
General approach is to parse early, to the most narrow type that is applicable at the point of parsing, and not at the point of using it. |
||
deriving (Eq, Ord, Show) | ||
|
||
instance Encoding DeviceToken where | ||
smpEncode (DeviceToken p t) = smpEncode (p, t) | ||
smpP = DeviceToken <$> smpP <*> smpP | ||
smpEncode token = case token of | ||
APNSDeviceToken p t -> smpEncode (p, t) | ||
WPDeviceToken t -> smpEncode (PPWebPush, t) | ||
smpP = do | ||
pp <- smpP | ||
case pp of | ||
PPWebPush -> WPDeviceToken <$> smpP | ||
_ -> APNSDeviceToken pp <$> smpP | ||
|
||
instance StrEncoding DeviceToken where | ||
strEncode (DeviceToken p t) = strEncode p <> " " <> t | ||
strP = nullToken <|> hexToken | ||
strEncode token = case token of | ||
APNSDeviceToken p t -> strEncode p <> " " <> t | ||
WPDeviceToken t -> strEncode PPWebPush <> " " <> strEncode t | ||
strP = nullToken <|> deviceToken | ||
where | ||
nullToken = "apns_null test_ntf_token" $> DeviceToken PPApnsNull "test_ntf_token" | ||
hexToken = DeviceToken <$> strP <* A.space <*> hexStringP | ||
nullToken = "apns_null test_ntf_token" $> APNSDeviceToken PPApnsNull "test_ntf_token" | ||
deviceToken = do | ||
pp <- strP_ | ||
case pp of | ||
PPWebPush -> WPDeviceToken <$> strP | ||
_ -> APNSDeviceToken pp <$> hexStringP | ||
hexStringP = | ||
A.takeWhile (`B.elem` "0123456789abcdef") >>= \s -> | ||
if even (B.length s) then pure s else fail "odd number of hex characters" | ||
|
||
instance ToJSON DeviceToken where | ||
toEncoding (DeviceToken pp t) = J.pairs $ "pushProvider" .= decodeLatin1 (strEncode pp) <> "token" .= decodeLatin1 t | ||
toJSON (DeviceToken pp t) = J.object ["pushProvider" .= decodeLatin1 (strEncode pp), "token" .= decodeLatin1 t] | ||
toEncoding token = case token of | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need JSON encodings for the token? If we do, then we need to use derived ToJSON/FromJson instances. Possibly, it is only needed for webpush tokens? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, this is used for the APNS token on iOS. So I have extended the function to webpush tokens as well |
||
APNSDeviceToken pp t -> J.pairs $ "pushProvider" .= decodeLatin1 (strEncode pp) <> "token" .= decodeLatin1 t | ||
WPDeviceToken t -> J.pairs $ "pushProvider" .= decodeLatin1 (strEncode PPWebPush) <> "token" .= toJSON t | ||
toJSON token = case token of | ||
APNSDeviceToken pp t -> J.object ["pushProvider" .= decodeLatin1 (strEncode pp), "token" .= decodeLatin1 t] | ||
WPDeviceToken t -> J.object ["pushProvider" .= decodeLatin1 (strEncode PPWebPush), "token" .= toJSON t] | ||
|
||
instance FromJSON DeviceToken where | ||
parseJSON = J.withObject "DeviceToken" $ \o -> do | ||
pp <- strDecode . encodeUtf8 <$?> o .: "pushProvider" | ||
t <- encodeUtf8 <$> o .: "token" | ||
pure $ DeviceToken pp t | ||
case pp of | ||
PPWebPush -> do | ||
WPDeviceToken <$> (o .: "token") | ||
_ -> do | ||
t <- encodeUtf8 <$> (o .: "token") | ||
pure $ APNSDeviceToken pp t | ||
|
||
-- | Returns fields for the device token (pushProvider, token) | ||
deviceTokenFields :: DeviceToken -> (PushProvider, ByteString) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function seems unnecessary, as it's better to store components as separate fields and split/combine when saving to the database. |
||
deviceTokenFields dt = case dt of | ||
APNSDeviceToken pp t -> (pp, t) | ||
WPDeviceToken t -> (PPWebPush, strEncode t) | ||
|
||
-- | Returns the device token from the fields (pushProvider, token) | ||
deviceToken' :: PushProvider -> ByteString -> DeviceToken | ||
deviceToken' pp t = case pp of | ||
PPWebPush -> WPDeviceToken <$> either error id $ strDecode t | ||
_ -> APNSDeviceToken pp t | ||
|
||
-- List of PNMessageData uses semicolon-separated encoding instead of strEncode, | ||
-- because strEncode of NonEmpty list uses comma for separator, | ||
|
Uh oh!
There was an error while loading. Please reload this page.