mariadb-haskell-0.1.0.0: src/Database/MySQL/Protocol/Auth.hs
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -funbox-strict-fields #-}
{-|
Module : Database.MySQL.Protocol.Auth
Description : Native MariaDB handshake and authentication packets
Copyright : (c) Winterland, 2016
License : BSD
Maintainer : Anton Antich <anton.antich@gmail.com>
Stability : experimental
Portability : PORTABLE
Auth related packet.
-}
module Database.MySQL.Protocol.Auth where
import Control.Monad
import Data.Binary
import Data.Binary.Get
import Data.Binary.Parser
import Data.Binary.Put
import qualified Data.ByteString as B
import Data.ByteString.Char8 as BC
import Data.Bits
import Database.MySQL.Protocol.Packet
--------------------------------------------------------------------------------
-- Authentications
#define CLIENT_LONG_PASSWORD 0x00000001
#define CLIENT_FOUND_ROWS 0x00000002
#define CLIENT_LONG_FLAG 0x00000004
#define CLIENT_CONNECT_WITH_DB 0x00000008
#define CLIENT_NO_SCHEMA 0x00000010
#define CLIENT_COMPRESS 0x00000020
#define CLIENT_ODBC 0x00000040
#define CLIENT_LOCAL_FILES 0x00000080
#define CLIENT_IGNORE_SPACE 0x00000100
#define CLIENT_PROTOCOL_41 0x00000200
#define CLIENT_INTERACTIVE 0x00000400
#define CLIENT_SSL 0x00000800
#define CLIENT_IGNORE_SIGPIPE 0x00001000
#define CLIENT_TRANSACTIONS 0x00002000
#define CLIENT_RESERVED 0x00004000
#define CLIENT_SECURE_CONNECTION 0x00008000
#define CLIENT_MULTI_STATEMENTS 0x00010000
#define CLIENT_MULTI_RESULTS 0x00020000
#define CLIENT_PS_MULTI_RESULTS 0x00040000
#define CLIENT_PLUGIN_AUTH 0x00080000
#define CLIENT_CONNECT_ATTRS 0x00100000
#define CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 0x00200000
-- | Native MariaDB protocol-10 greeting. Capabilities retain both 32-bit words;
-- the salt fields are exact binary challenge bytes without a wire terminator.
data Greeting = Greeting
{ greetingProtocol :: !Word8
, greetingVersion :: !B.ByteString
, greetingConnId :: !Word32
, greetingSalt1 :: !B.ByteString
, greetingCaps :: !Word64
, greetingCharset :: !Word8
, greetingStatus :: !Word16
, greetingSalt2 :: !B.ByteString
, greetingAuthPlugin :: !B.ByteString
} deriving (Show, Eq)
-- | Encode the MariaDB greeting, including the upper capability word. Salt
-- fields contain exact challenge bytes, without the protocol terminator.
putGreeting :: Greeting -> Put
putGreeting (Greeting pv sv cid salt1 cap charset st salt2 plugin) = do
putWord8 pv
putByteString sv >> putWord8 0
putWord32le cid
putByteString salt1 >> putWord8 0
putWord16le (fromIntegral cap)
putWord8 charset
putWord16le st
putWord16le (fromIntegral (cap `shiftR` 16))
putWord8 (fromIntegral (B.length salt1 + B.length salt2 + 1))
replicateM_ 6 (putWord8 0)
putWord32le (fromIntegral (cap `shiftR` 32))
when (cap .&. CLIENT_SECURE_CONNECTION /= 0) (putByteString salt2 >> putWord8 0)
when (cap .&. CLIENT_PLUGIN_AUTH /= 0) (putByteString plugin >> putWord8 0)
-- | Parse by the declared challenge length. A binary nonce may contain NUL;
-- only a plugin's known protocol terminator is removed, never an arbitrary byte.
getGreeting :: Get Greeting
getGreeting = do
pv <- getWord8
unless (pv == 10) (fail "unsupported MariaDB greeting protocol")
sv <- getByteStringNul
cid <- getWord32le
salt1 <- getByteString 8
skipN 1
capL <- getWord16le
charset <- getWord8
status <- getWord16le
capH <- getWord16le
let low = fromIntegral capH `shiftL` 16 .|. fromIntegral capL
challengeLength <- fromIntegral <$> getWord8
skipN 6
upper <- getWord32le
let cap = low .|. if testBit low 0 then 0 else fromIntegral upper `shiftL` 32
unless (cap .&. 0x88200 == 0x88200) (fail "MariaDB requires protocol-41, secure challenge and plugin authentication")
salt2 <- if cap .&. CLIENT_SECURE_CONNECTION == 0 then pure B.empty
else getByteString (if challengeLength >= 8 then challengeLength - 8 else 13)
plugin <- if cap .&. CLIENT_PLUGIN_AUTH == 0 then pure "mysql_native_password"
else getByteStringNul
when (B.null plugin || B.length plugin > 128) (fail "invalid authentication plugin name")
let exactLength = if plugin == "client_ed25519" || plugin == "parsec" then 32 else 20
challenge = salt1 <> salt2
exact = if B.length challenge == exactLength + 1 && B.last challenge == 0
then B.init challenge else challenge
pure (Greeting pv sv cid (B.take 8 exact) cap charset status (B.drop 8 exact) plugin)
instance Binary Greeting where
get = getGreeting
put = putGreeting
-- | Client response. Only implemented, server-offered capabilities belong here;
-- the upper word is emitted in reserved bytes 28..31 for a native client.
data Auth = Auth
{ authCaps :: !Word64
, authMaxPacket :: !Word32
, authCharset :: !Word8
, authName :: !ByteString
, authPassword :: !ByteString
, authSchema :: !ByteString
, authPlugin :: !ByteString
} deriving (Show, Eq)
-- | Decode the fixed client header. The extension word occupies bytes 28..31 of both client handshake forms.
-- Legacy-marked peers use zero reserved bytes instead of an extension word.
getClientHeader :: Get (Word64, Word32, Word8)
getClientHeader = do
low <- getWord32le
size <- getWord32le
charset <- getWord8
skipN 19
upper <- getWord32le
pure (fromIntegral low .|. if testBit low 0 then 0 else fromIntegral upper `shiftL` 32, size, charset)
-- | Encode the fixed header shared by Auth and SSLRequest. Native extension
-- bits occupy bytes 28..31; legacy-marked packets leave that word zero.
putClientHeader :: Word64 -> Word32 -> Word8 -> Put
putClientHeader caps size charset = do
putWord32le (fromIntegral caps)
putWord32le size
putWord8 charset
replicateM_ 19 (putWord8 0)
putWord32le (if testBit caps 0 then 0 else fromIntegral (caps `shiftR` 32))
-- | Decode this client's secure-connection response format. The password
-- response is one-byte length-delimited; database/plugin fields follow flags.
getAuth :: Get Auth
getAuth = do
(caps, size, charset) <- getClientHeader
name <- getByteStringNul
len <- fromIntegral <$> getWord8
password <- getByteString len
schema <- if testBit caps 3 then getByteStringNul else pure B.empty
plugin <- if testBit caps 19 then getByteStringNul else pure B.empty
pure (Auth caps size charset name password schema plugin)
-- | Encode a response with a password payload of at most 255 bytes. The
-- connected plugin dispatcher produces bounded payloads and negotiates flags.
putAuth :: Auth -> Put
putAuth (Auth caps size charset name password schema plugin) = do
putClientHeader caps size charset
putByteString name >> putWord8 0
putWord8 (fromIntegral (B.length password))
putByteString password
when (testBit caps 3) (putByteString schema >> putWord8 0)
when (testBit caps 19) (putByteString plugin >> putWord8 0)
instance Binary Auth where
get = getAuth
put = putAuth
-- | The 32-byte pre-TLS request uses the same capability layout as Auth.
-- Its bits must agree with the following authenticated TLS response.
data SSLRequest = SSLRequest
{ sslReqCaps :: !Word64
, sslReqMaxPacket :: !Word32
, sslReqCharset :: !Word8
} deriving (Show, Eq)
-- | Decode the complete fixed portion of the pre-TLS request. Callers
-- validating an envelope should require exact packet consumption.
getSSLRequest :: Get SSLRequest
getSSLRequest = do
(caps, size, charset) <- getClientHeader
pure (SSLRequest caps size charset)
-- | Encode the 32-byte request sent before starting the TLS handshake.
putSSLRequest :: SSLRequest -> Put
putSSLRequest (SSLRequest caps size charset) = putClientHeader caps size charset
instance Binary SSLRequest where
get = getSSLRequest
put = putSSLRequest
--------------------------------------------------------------------------------
-- default Capability Flags
-- | Implemented ordinary capabilities before intersection with the server.
-- Native extension features remain off until their readers are implemented.
clientCap :: Word64
-- Bit zero is MariaDB's legacy-client marker. Clearing it enables the
-- extension-word layout; unimplemented high-word features stay disabled.
clientCap = CLIENT_LONG_FLAG
.|. CLIENT_CONNECT_WITH_DB
.|. CLIENT_IGNORE_SPACE
.|. CLIENT_PROTOCOL_41
.|. CLIENT_TRANSACTIONS
.|. CLIENT_MULTI_STATEMENTS
.|. CLIENT_MULTI_RESULTS
.|. CLIENT_PS_MULTI_RESULTS
.|. CLIENT_SECURE_CONNECTION
.|. CLIENT_PLUGIN_AUTH
-- | Historical low-level default; connected APIs advertise the configured
-- logical-message limit from ConnectionOptions instead.
clientMaxPacketSize :: Word32
clientMaxPacketSize = 0x00ffffff :: Word32
-- | Whether the greeting offers TLS, without enabling it or downgrading.
supportTLS :: Word64 -> Bool
supportTLS x = (x .&. CLIENT_SSL) /= 0
-- | Low-level default request builder. Live constructors instead intersect
-- capabilities with their server and use the configured packet limit.
sslRequest :: Word8 -> SSLRequest
sslRequest charset = SSLRequest (clientCap .|. CLIENT_SSL) clientMaxPacketSize charset