{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Network.TLS.Handshake.Client.ClientHello (
    sendClientHello,
    getPreSharedKeyInfo,
) where

import qualified Control.Exception as E
import Crypto.HPKE
import qualified Data.ByteString as B
import Network.TLS.ECH.Config
import System.Random

#if !MIN_VERSION_random(1,3,0)
import Data.ByteString.Internal (unsafeCreate)
import Foreign.Ptr
import Foreign.Storable
#endif

import Network.TLS.Cipher
import Network.TLS.Context.Internal
import Network.TLS.Crypto
import Network.TLS.Extension
import Network.TLS.Handshake.Client.Common
import Network.TLS.Handshake.Common
import Network.TLS.Handshake.Common13
import Network.TLS.Handshake.Control
import Network.TLS.Handshake.Random
import Network.TLS.Handshake.State
import Network.TLS.Handshake.State13
import Network.TLS.Handshake.TranscriptHash
import Network.TLS.IO
import Network.TLS.Imports
import Network.TLS.Packet hiding (getExtensions)
import Network.TLS.Parameters
import Network.TLS.State
import Network.TLS.Struct
import Network.TLS.Types

----------------------------------------------------------------

sendClientHello
    :: ClientParams
    -> Context
    -> [Group]
    -> Maybe (ClientRandom, Session, Version)
    -> PreSharedKeyInfo
    -> IO ClientRandom
sendClientHello cparams ctx groups mparams pskinfo = do
    crand <- generateClientHelloParams mparams -- Inner for ECH
    sendClientHello' cparams ctx groups crand pskinfo
    return crand
  where
    highestVer = maximum $ supportedVersions $ ctxSupported ctx
    tls13 = highestVer >= TLS13
    ems = supportedExtendedMainSecret $ ctxSupported ctx

    -- Client random and session in the second client hello for
    -- retry must be the same as the first one.
    generateClientHelloParams (Just (crand, clientSession, _)) = do
        modifyTLS13State ctx $ \st -> st{tls13stSession = clientSession}
        return crand
    generateClientHelloParams Nothing = do
        crand <- clientRandom ctx
        let paramSession = case clientSessions cparams of
                [] -> Session Nothing
                (sidOrTkt, sdata) : _
                    | sessionVersion sdata >= TLS13 -> Session Nothing
                    | ems == RequireEMS && noSessionEMS -> Session Nothing
                    | isTicket sidOrTkt -> Session $ Just $ toSessionID sidOrTkt
                    | otherwise -> Session (Just sidOrTkt)
                  where
                    noSessionEMS = SessionEMS `notElem` sessionFlags sdata
        -- In compatibility mode a client not offering a pre-TLS 1.3
        -- session MUST generate a new 32-byte value
        if tls13 && paramSession == Session Nothing && not (ctxQUICMode ctx)
            then do
                randomSession <- newSession ctx
                modifyTLS13State ctx $ \st -> st{tls13stSession = randomSession}
                return crand
            else do
                modifyTLS13State ctx $ \st -> st{tls13stSession = paramSession}
                return crand

----------------------------------------------------------------

sendClientHello'
    :: ClientParams
    -> Context
    -> [Group]
    -> ClientRandom
    -> ( Maybe ([ByteString], SessionData, CipherChoice, Word32)
       , Maybe CipherChoice
       , Bool
       )
    -> IO ()
sendClientHello' cparams ctx groups crand (pskInfo, rtt0info, rtt0) = do
    let ver = if tls13 then TLS12 else highestVer
    clientSession <- tls13stSession <$> getTLS13State ctx
    hrr <- usingState_ ctx getTLS13HRR
    unless hrr $ startHandshake ctx ver crand
    usingState_ ctx $ setVersionIfUnset highestVer
    let cipherIds = map (CipherId . cipherID) ciphers
        mkClientHello exts =
            CH
                { chVersion = ver
                , chRandom = crand
                , chSession = clientSession
                , chCiphers = cipherIds
                , chComps = [0]
                , chExtensions = exts
                }
    setMyRecordLimit ctx $ limitRecordSize $ sharedLimit $ ctxShared ctx
    extensions0 <- catMaybes <$> getExtensions
    let extensions1 = sharedHelloExtensions (clientShared cparams) ++ extensions0
    extensions <- adjustPreSharedKeyExt extensions1 $ mkClientHello extensions1
    let ch0 = mkClientHello extensions
    updateTranscriptHashI ctx "ClientHelloI" $ encodeHandshake $ ClientHello ch0
    let nhpks = supportedHPKE $ clientSupported cparams
        echcnfs = sharedECHConfigList $ clientShared cparams
        mEchParams = lookupECHConfigList nhpks echcnfs
    ch <-
        if clientUseECH cparams
            then case mEchParams of
                Nothing -> do
                    if hrr
                        then do
                            chI <- fromJust <$> usingHState ctx getClientHello
                            let ch0' = ch0{chExtensions = take 1 (chExtensions chI) ++ drop 1 (chExtensions ch0)}
                            usingHState ctx $ setClientHello ch0'
                            return ch0'
                        else do
                            gEchExt <- greasingEchExt
                            let ch0' = ch0{chExtensions = gEchExt : drop 1 (chExtensions ch0)}
                            usingHState ctx $ setClientHello ch0'
                            return ch0'
                Just echParams -> do
                    usingHState ctx $ setClientHello ch0
                    mcrandO <- usingHState ctx getOuterClientRandom
                    crandO <- case mcrandO of
                        Nothing -> clientRandom ctx
                        Just x -> return x
                    usingHState ctx $ do
                        setClientRandom crandO
                        setOuterClientRandom $ Just crandO
                    mpskExt <- randomPreSharedKeyExt
                    createEncryptedClientHello ctx ch0 echParams crandO mpskExt
            else do
                usingHState ctx $ setClientHello ch0
                return ch0
    sendPacket12 ctx $ Handshake [ClientHello ch]
    mEarlySecInfo <- case rtt0info of
        Nothing -> return Nothing
        Just info -> Just <$> getEarlySecretInfo info
    unless hrr $ contextSync ctx $ SendClientHello mEarlySecInfo
    let sentExtensions = map (\(ExtensionRaw i _) -> i) extensions
    modifyTLS13State ctx $ \st -> st{tls13stSentExtensions = sentExtensions}
  where
    ciphers = supportedCiphers $ ctxSupported ctx
    highestVer = maximum $ supportedVersions $ ctxSupported ctx
    tls13 = highestVer >= TLS13
    ems = supportedExtendedMainSecret $ ctxSupported ctx
    groupToSend = listToMaybe groups

    -- List of extensions to send in ClientHello, ordered such that we never
    -- terminate with a zero-length extension.  Some buggy implementations
    -- are allergic to an extension with empty data at final position.
    --
    -- Without TLS 1.3, the list ends with extension "signature_algorithms"
    -- with length >= 2 bytes.  When TLS 1.3 is enabled, extensions
    -- "psk_key_exchange_modes" (currently always sent) and "pre_shared_key"
    -- (not always present) have length > 0.
    getExtensions =
        sequence
            [ {- 0xfe0d -} echExt
            , {- 0x00 -} sniExt
            , {- 0x0a -} groupExt
            , {- 0x0b -} ecPointExt
            , {- 0x0d -} signatureAlgExt
            , {- 0x10 -} alpnExt
            , {- 0x17 -} emsExt
            , {- 0x1b -} compCertExt
            , {- 0x1c -} recordSizeLimitExt
            , {- 0x23 -} sessionTicketExt
            , {- 0x2a -} earlyDataExt
            , {- 0x2b -} versionExt
            , {- 0x2c -} cookieExt
            , {- 0x2d -} pskExchangeModeExt
            , {- 0x31 -} postHandshakeAuthExt
            , {- 0x33 -} keyShareExt
            , {- 0xff01 -} secureRenegExt
            , {- 0x29 -} preSharedKeyExt -- MUST be last (RFC 8446)
            ]

    --------------------

    sniExt =
        if clientUseServerNameIndication cparams
            then do
                let sni = fst $ clientServerIdentification cparams
                usingState_ ctx $ setClientSNI sni
                return $ Just $ toExtensionRaw $ ServerName [ServerNameHostName sni]
            else return Nothing

    groupExt =
        return $
            Just $
                toExtensionRaw $
                    SupportedGroups (supportedGroups $ ctxSupported ctx)

    ecPointExt =
        return $
            Just $
                toExtensionRaw $
                    EcPointFormatsSupported [EcPointFormat_Uncompressed]

    signatureAlgExt =
        return $
            Just $
                toExtensionRaw $
                    SignatureAlgorithms $
                        supportedHashSignatures $
                            clientSupported cparams

    alpnExt = do
        mprotos <- onSuggestALPN $ clientHooks cparams
        case mprotos of
            Nothing -> return Nothing
            Just protos -> do
                usingState_ ctx $ setClientALPNSuggest protos
                return $ Just $ toExtensionRaw $ ApplicationLayerProtocolNegotiation protos

    emsExt =
        return $
            if ems == NoEMS || all (>= TLS13) (supportedVersions $ ctxSupported ctx)
                then Nothing
                else Just $ toExtensionRaw ExtendedMainSecret

    compCertExt = return $ Just $ toExtensionRaw (CompressCertificate [CCA_Zlib])

    recordSizeLimitExt = case limitRecordSize $ sharedLimit $ ctxShared ctx of
        Nothing -> return Nothing
        Just siz -> return $ Just $ toExtensionRaw $ RecordSizeLimit $ fromIntegral siz

    sessionTicketExt = do
        case clientSessions cparams of
            (sidOrTkt, _) : _
                | isTicket sidOrTkt -> return $ Just $ toExtensionRaw $ SessionTicket sidOrTkt
            _ -> return $ Just $ toExtensionRaw $ SessionTicket ""

    earlyDataExt
        | rtt0 = return $ Just $ toExtensionRaw (EarlyDataIndication Nothing)
        | otherwise = return Nothing

    versionExt
        | clientUseECH cparams = do
            let vers = supportedVersions $ ctxSupported ctx
            if TLS13 `elem` vers
                then
                    return $ Just $ toExtensionRaw $ SupportedVersionsClientHello [TLS13]
                else
                    throwCore $ Error_Misc "TLS 1.3 must be specified for Encrypted Client Hello"
        | tls13 = do
            let vers = filter (>= TLS12) $ supportedVersions $ ctxSupported ctx
            return $ Just $ toExtensionRaw $ SupportedVersionsClientHello vers
        | otherwise = return Nothing

    cookieExt = do
        mcookie <- usingState_ ctx getTLS13Cookie
        case mcookie of
            Nothing -> return Nothing
            Just cookie -> return $ Just $ toExtensionRaw cookie

    pskExchangeModeExt
        | tls13 = return $ Just $ toExtensionRaw $ PskKeyExchangeModes [PSK_DHE_KE]
        | otherwise = return Nothing

    postHandshakeAuthExt
        | ctxQUICMode ctx = return Nothing
        | tls13 = return $ Just $ toExtensionRaw PostHandshakeAuth
        | otherwise = return Nothing

    -- FIXME
    keyShareExt
        | tls13 = case groupToSend of
            Nothing -> return Nothing
            Just grp -> do
                (cpri, ent) <- makeClientKeyShare ctx grp
                usingHState ctx $ setGroupPrivate cpri
                return $ Just $ toExtensionRaw $ KeyShareClientHello [ent]
        | otherwise = return Nothing

    secureRenegExt =
        if supportedSecureRenegotiation $ ctxSupported ctx
            then do
                VerifyData cvd <- usingState_ ctx $ getVerifyData ClientRole
                return $ Just $ toExtensionRaw $ SecureRenegotiation cvd ""
            else return Nothing

    -- ECHClientHelloInner should be replaced if ECHConfigList is not available.
    echExt
        | clientUseECH cparams = return $ Just $ toExtensionRaw ECHClientHelloInner
        | otherwise = return Nothing

    preSharedKeyExt =
        case pskInfo of
            Nothing -> return Nothing
            Just (identities, _, choice, obfAge) -> do
                let zero = cZero choice
                    pskIdentities = map (\x -> PskIdentity x obfAge) identities
                    -- [zero] is a place holds.
                    -- adjustPreSharedKeyExt will replace them.
                    binders = replicate (length pskIdentities) zero
                    offeredPsks = PreSharedKeyClientHello pskIdentities binders
                return $ Just $ toExtensionRaw offeredPsks

    randomPreSharedKeyExt :: IO (Maybe ExtensionRaw)
    randomPreSharedKeyExt =
        case pskInfo of
            Nothing -> return Nothing
            Just (identities, _, choice, _) -> do
                let zero = cZero choice
                zeroR <- getStdRandom $ uniformByteString $ B.length zero
                obfAgeR <- getStdRandom genWord32
                let genPskId x = do
                        xR <- getStdRandom $ uniformByteString $ B.length x
                        return $ PskIdentity xR obfAgeR
                pskIdentitiesR <- mapM genPskId identities
                let bindersR = replicate (length pskIdentitiesR) zeroR
                    offeredPsksR = PreSharedKeyClientHello pskIdentitiesR bindersR
                return $ Just $ toExtensionRaw offeredPsksR

    ----------------------------------------

    adjustPreSharedKeyExt exts ch =
        case pskInfo of
            Nothing -> return exts
            Just (identities, sdata, choice, _) -> do
                let psk = sessionSecret sdata
                    earlySecret = initEarlySecret choice (Just psk)
                usingHState ctx $ setTLS13EarlySecret earlySecret
                let ech = encodeHandshake $ ClientHello ch
                    h = cHash choice
                    siz = (hashDigestSize h + 1) * length identities + 2
                    binder = makePSKBinder earlySecret h siz ech
                -- PSK is shared by the previous TLS session.
                -- So, PSK is unique for identities.
                let binders = replicate (length identities) binder
                let exts' = init exts ++ [adjust (last exts)]
                    adjust (ExtensionRaw eid withoutBinders) = ExtensionRaw eid withBinders
                      where
                        withBinders = replacePSKBinder withoutBinders binders
                return exts'

    getEarlySecretInfo choice = do
        let usedCipher = cCipher choice
            usedHash = cHash choice
        Just earlySecret <- usingHState ctx getTLS13EarlySecret
        earlyKey <- calculateEarlySecret ctx choice (Right earlySecret)
        let clientEarlySecret = pairClient earlyKey
        unless (ctxQUICMode ctx) $ do
            runPacketFlight ctx $ sendChangeCipherSpec13 ctx
            setTxRecordState ctx usedHash usedCipher clientEarlySecret
            setEstablished ctx EarlyDataSending
        -- We set RTT0Sent even in quicMode
        usingHState ctx $ setTLS13RTT0Status RTT0Sent
        return $ EarlySecretInfo usedCipher clientEarlySecret

----------------------------------------------------------------

type PreSharedKeyInfo =
    ( Maybe ([SessionIDorTicket], SessionData, CipherChoice, Second)
    , Maybe CipherChoice
    , Bool
    )

getPreSharedKeyInfo
    :: ClientParams
    -> Context
    -> IO PreSharedKeyInfo
getPreSharedKeyInfo cparams ctx = do
    pskInfo <- getPskInfo
    let rtt0info = pskInfo >>= get0RTTinfo
        rtt0 = isJust rtt0info
    return (pskInfo, rtt0info, rtt0)
  where
    ciphers = supportedCiphers $ ctxSupported ctx
    highestVer = maximum $ supportedVersions $ ctxSupported ctx
    tls13 = highestVer >= TLS13

    sessions = case clientSessions cparams of
        [] -> Nothing
        (sid, sdata) : xs -> do
            guard tls13
            guard (sessionVersion sdata >= TLS13)
            let cid = sessionCipher sdata
                sids = map fst xs
            sCipher <- findCipher cid ciphers
            Just (sid : sids, sdata, sCipher)

    getPskInfo = case sessions of
        Nothing -> return Nothing
        Just (identity, sdata, sCipher) -> do
            let tinfo = fromJust $ sessionTicketInfo sdata
            age <- getAge tinfo
            return $
                if isAgeValid age tinfo
                    then
                        Just
                            ( identity
                            , sdata
                            , makeCipherChoice TLS13 sCipher
                            , ageToObfuscatedAge age tinfo
                            )
                    else Nothing

    get0RTTinfo (_, sdata, choice, _)
        | clientUseEarlyData cparams && sessionMaxEarlyDataSize sdata > 0 = Just choice
        | otherwise = Nothing

----------------------------------------------------------------

createEncryptedClientHello
    :: Context
    -> ClientHello
    -> (KDF_ID, AEAD_ID, ECHConfig)
    -> ClientRandom
    -> Maybe ExtensionRaw
    -> IO ClientHello
createEncryptedClientHello ctx ch0@CH{..} echParams@(kdfid, aeadid, conf) crO mpskExt = E.handle hpkeHandler $ do
    let (chExtsO, chExtsI) = dupCompExts (cnfPublicName conf) mpskExt chExtensions
        chI =
            ch0
                { chSession = Session Nothing
                , chExtensions = chExtsI
                }
    Just (func, enc, taglen) <- getHPKE ctx echParams
    let bsI = encodeHandshake' $ ClientHello chI
        padLen = 32 - (B.length bsI .&. 31)
        bsI' = bsI <> B.replicate padLen 0
    let outerZ =
            ECHClientHelloOuter
                { echCipherSuite = (kdfid, aeadid)
                , echConfigId = cnfConfigId conf
                , echEnc = enc
                , echPayload = B.replicate (B.length bsI' + taglen) 0
                }
        echOZ = extensionEncode outerZ
        chExtsOTail = drop 1 chExtsO
        chOZ =
            ch0
                { chRandom = crO
                , chExtensions =
                    ExtensionRaw EID_EncryptedClientHello echOZ : chExtsOTail
                }
        aad = encodeHandshake' $ ClientHello chOZ
    bsO <- func aad bsI'
    let outer =
            ECHClientHelloOuter
                { echCipherSuite = (kdfid, aeadid)
                , echConfigId = cnfConfigId conf
                , echEnc = enc
                , echPayload = bsO
                }
        echO = extensionEncode outer
        chO =
            chOZ
                { chExtensions =
                    ExtensionRaw EID_EncryptedClientHello echO : chExtsOTail
                }
    return chO
  where
    hpkeHandler :: HPKEError -> IO ClientHello
    hpkeHandler _ = return ch0

dupCompExts
    :: HostName
    -> Maybe ExtensionRaw
    -> [ExtensionRaw]
    -> ([ExtensionRaw], [ExtensionRaw]) -- Outer, inner
dupCompExts host mpskExt chExts = step1 chExts
  where
    step1 (echExtI@(ExtensionRaw EID_EncryptedClientHello _) : exts) =
        (echExtO : os, echExtI : is)
      where
        echExtO = ExtensionRaw EID_EncryptedClientHello ""
        (os, is) = step2 exts
    step1 _ = error "step1"
    step2 (sniExtI@(ExtensionRaw EID_ServerName _) : exts) =
        (sniExtO : os, sniExtI : is)
      where
        sniExtO = toExtensionRaw $ ServerName [ServerNameHostName host]
        (os, is) = step3 exts id
    step2 _ = error "step2"
    step3 [] build = ([], [echOuterExt])
      where
        echOuterExt = toExtensionRaw $ EchOuterExtensions $ build []
    step3 [pskExtI@(ExtensionRaw EID_PreSharedKey _)] build =
        ([pskExtO], [echOuterExt, pskExtI])
      where
        echOuterExt = toExtensionRaw $ EchOuterExtensions $ build []
        pskExtO = fromJust mpskExt
    step3 (i@(ExtensionRaw eid _) : is) build = (i : os', is')
      where
        (os', is') = step3 is (build . (eid :))

getHPKE
    :: Context
    -> (KDF_ID, AEAD_ID, ECHConfig)
    -> IO (Maybe (AAD -> PlainText -> IO CipherText, EncodedPublicKey, Int))
getHPKE ctx (kdfid, aeadid, conf) = do
    mfunc <- getTLS13HPKE ctx
    case mfunc of
        Nothing -> do
            let encodedConfig = encodeECHConfig conf
                info = "tls ech\x00" <> encodedConfig
            (pkSm, ctxS) <- setupBaseS kemid kdfid aeadid Nothing Nothing mpkR info
            let func = seal ctxS
            setTLS13HPKE ctx func 0
            return $ Just (func, pkSm, nT)
        Just (func, _) -> return $ Just (func, EncodedPublicKey "", nT)
  where
    mpkR = cnfEncodedPublicKey conf
    kemid = cnfKemId conf
    nT = nTag aeadid

----------------------------------------------------------------

lookupECHConfigList
    :: [(KEM_ID, KDF_ID, AEAD_ID)]
    -> ECHConfigList
    -> Maybe (KDF_ID, AEAD_ID, ECHConfig)
lookupECHConfigList [] _ = Nothing
lookupECHConfigList ((kemid, kdfid, aeadid) : xs) cnfs =
    case find (\cnf -> cnfKemId cnf == kemid) cnfs of
        Nothing -> lookupECHConfigList xs cnfs
        Just cnf
            | (kdfid, aeadid) `elem` cnfCipherSuite cnf ->
                Just (kdfid, aeadid, cnf)
            | otherwise -> lookupECHConfigList xs cnfs

cnfKemId :: ECHConfig -> KEM_ID
cnfKemId ECHConfig{..} = KEM_ID $ kem_id $ key_config contents

cnfCipherSuite :: ECHConfig -> [(KDF_ID, AEAD_ID)]
cnfCipherSuite ECHConfig{..} = map conv $ cipher_suites $ key_config contents
  where
    conv HpkeSymmetricCipherSuite{..} = (KDF_ID kdf_id, AEAD_ID aead_id)

cnfEncodedPublicKey :: ECHConfig -> EncodedPublicKey
cnfEncodedPublicKey ECHConfig{..} = EncodedPublicKey pk
  where
    EncodedServerPublicKey pk = public_key $ key_config contents

cnfPublicName :: ECHConfig -> HostName
cnfPublicName ECHConfig{..} = public_name contents

cnfConfigId :: ECHConfig -> ConfigId
cnfConfigId ECHConfig{..} = config_id $ key_config contents

----------------------------------------------------------------

-- Pretending X25519 is used because it is the de-facto and
-- its public key is easily created.
greasingEchExt :: IO ExtensionRaw
greasingEchExt = do
    cid <- getStdRandom genWord8
    enc <- getStdRandom $ uniformByteString 32
    n <- getStdRandom $ randomR (4, 6)
    payload <- getStdRandom $ uniformByteString (n * 32 + 16)
    let outer =
            ECHClientHelloOuter
                { echCipherSuite = (HKDF_SHA256, AES_128_GCM)
                , echConfigId = cid
                , echEnc = EncodedPublicKey enc
                , echPayload = payload
                }
    return $ toExtensionRaw outer

#if !MIN_VERSION_random(1,3,0)
uniformByteString :: RandomGen g => Int -> g -> (ByteString, g)
uniformByteString l g0 = (bs, g2)
  where
    (g1, g2) = split g0
    bs = unsafeCreate l $ go 0 g1
    go n g ptr
        | n == l = return ()
        | otherwise = do
            let (w, g') = genWord8 g
            poke ptr w
            go (n + 1) g' (plusPtr ptr 1)
#endif
