module Web.WebPush.Internal where
import GHC.Int (Int64)
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 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.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 Data.Aeson (ToJSON, toJSON, (.=))
import qualified Data.Aeson as A
import qualified Data.ByteString.Base64.URL as B64.URL
import Data.Word (Word8, Word16, Word64)
import qualified Data.Binary as Binary
import qualified Data.Bits as Bits
import qualified Data.ByteArray as ByteArray
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Except (runExceptT)
type VAPIDKeys = ECDSA.KeyPair
data VAPIDClaims = VAPIDClaims { vapidAud :: JWT.Audience
, vapidSub :: JWT.StringOrURI
, vapidExp :: JWT.NumericDate
}
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
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
]
data WebPushEncryptionInput = EncryptionInput { applicationServerPrivateKey :: ECDH.PrivateNumber
, userAgentPublicKeyBytes :: ByteString
, authenticationSecret :: ByteString
, salt :: ByteString
, plainText :: LB.ByteString
, paddingLength :: Int64
}
data WebPushEncryptionOutput = EncryptionOutput { sharedECDHSecretBytes :: ByteString
, inputKeyingMaterialBytes :: ByteString
, contentEncryptionKeyContext :: ByteString
, contentEncryptionKey :: ByteString
, nonceContext :: ByteString
, nonce :: ByteString
, paddedPlainText :: ByteString
, encryptedMessage :: ByteString
}
webPushEncrypt :: WebPushEncryptionInput -> Either CryptoError WebPushEncryptionOutput
webPushEncrypt EncryptionInput {..} =
let applicationServerPublicKeyBytes = LB.toStrict $ ecPublicKeyToBytes $
ECDH.calculatePublic (ECC.getCurveByName ECC.SEC_p256r1) $
applicationServerPrivateKey
userAgentPublicKey = ecBytesToPublicKey userAgentPublicKeyBytes
sharedECDHSecret = ECDH.getShared (ECC.getCurveByName ECC.SEC_p256r1) applicationServerPrivateKey userAgentPublicKey
pseudoRandomKeyCombine = HMAC.hmac authenticationSecret sharedECDHSecret :: HMAC.HMAC SHA256
authInfo = "Content-Encoding: auth" <> "\x00" :: ByteString
inputKeyingMaterial = HMAC.hmac pseudoRandomKeyCombine (authInfo <> "\x01") :: HMAC.HMAC SHA256
context = "P-256" <> "\x00" <>
"\x00" <> "\x41" <> userAgentPublicKeyBytes <>
"\x00" <> "\x41" <> applicationServerPublicKeyBytes
pseudoRandomKeyEncryption = HMAC.hmac salt inputKeyingMaterial :: HMAC.HMAC SHA256
contentEncryptionKeyContext = "Content-Encoding: aesgcm" <> "\x00" <> context
contentEncryptionKey = BS.pack $ take 16 $ ByteArray.unpack (HMAC.hmac pseudoRandomKeyEncryption (contentEncryptionKeyContext <> "\x01") :: HMAC.HMAC SHA256)
nonceContext = "Content-Encoding: nonce" <> "\x00" <> context
nonce = BS.pack $ take 12 $ ByteArray.unpack (HMAC.hmac pseudoRandomKeyEncryption (nonceContext <> "\x01") :: HMAC.HMAC SHA256)
inputKeyingMaterialBytes = ByteArray.convert inputKeyingMaterial
sharedECDHSecretBytes = ByteArray.convert sharedECDHSecret
paddedPlainText = LB.toStrict $
(Binary.encode (fromIntegral paddingLength :: Word16)) <>
(LB.replicate paddingLength (0 :: Word8)) <>
plainText
eitherAesCipher = Cipher.cipherInit contentEncryptionKey :: CryptoFailable AES128
in case eitherAesCipher of
CryptoFailed err -> Left err
CryptoPassed aesCipher ->
let eitherAeadGcmCipher = Cipher.aeadInit Cipher.AEAD_GCM aesCipher nonce
in case eitherAeadGcmCipher of
CryptoFailed err -> Left err
CryptoPassed aeadGcmCipher ->
let encryptedMessage = let (authTagBytes, cipherText) = Cipher.aeadSimpleEncrypt aeadGcmCipher
BS.empty
paddedPlainText
16
authTag = ByteArray.convert $ Cipher.unAuthTag authTagBytes
in cipherText <> authTag
in Right $ EncryptionOutput {..}
ecPublicKeyToBytes :: ECC.Point -> LB.ByteString
ecPublicKeyToBytes ECC.PointO = "\x04" <>
(Binary.encode $ int32Bytes 0) <>
(Binary.encode $ int32Bytes 0)
ecPublicKeyToBytes (ECC.Point x y) = "\x04" <>
(Binary.encode $ int32Bytes x) <>
(Binary.encode $ int32Bytes y)
ecBytesToPublicKey :: ByteString -> ECC.Point
ecBytesToPublicKey bytes = let bothCoordBytes = BS.drop 1 bytes
(xBytes, yBytes) = Binary.decode $ LB.fromStrict bothCoordBytes :: (Bytes32, Bytes32)
xInteger = bytes32Int xBytes
yInteger = bytes32Int yBytes
in ECC.Point xInteger yInteger
type Bytes32 = (Word64, Word64, Word64, Word64)
int32Bytes :: Integer -> Bytes32
int32Bytes number = let shift1 = Bits.shiftR number 64
shift2 = Bits.shiftR shift1 64
shift3 = Bits.shiftR shift2 64
in ( fromIntegral shift3
, fromIntegral shift2
, fromIntegral shift1
, fromIntegral number
)
bytes32Int :: Bytes32 -> Integer
bytes32Int (d,c,b,a) = (Bits.shiftL (fromIntegral d) (64*3)) +
(Bits.shiftL (fromIntegral c) (64*2)) +
(Bits.shiftL (fromIntegral b) (64 )) +
(fromIntegral a)
b64UrlNoPadding :: ByteString -> ByteString
b64UrlNoPadding = fst . BS.breakSubstring "=" . B64.URL.encode