@@ -14,7 +14,9 @@ This is an internal module, the 'MySQLConn' type should not directly acessed to
1414
1515-}
1616
17- module Database.MySQL.Connection where
17+ module Database.MySQL.Connection
18+ ( module Database.MySQL.Connection
19+ ) where
1820
1921import Control.Exception (Exception , bracketOnError ,
2022 throwIO , catch , SomeException )
@@ -124,37 +126,107 @@ connectDetail (ConnectInfo host port db user pass charset)
124126 let auth = mkAuth db user pass charset greet
125127 write c $ encodeToPacket 1 auth
126128 q <- readPacket is'
127- if isOK q
128- then do
129- consumed <- newIORef True
130- let waitNotMandatoryOK = catch
131- (void (waitCommandReply is')) -- server will either reply an OK packet
132- ((\ _ -> return () ) :: SomeException -> IO () ) -- or directy close the connection
133- conn = MySQLConn is'
134- (write c)
135- (writeCommand COM_QUIT (write c) >> waitNotMandatoryOK >> TCP. close c)
136- consumed
137- return (greet, conn)
138- else TCP. close c >> decodeFromPacket q >>= throwIO . ERRException
129+ completeAuth is' (write c) pass q plainFullAuth
130+ consumed <- newIORef True
131+ let waitNotMandatoryOK = catch
132+ (void (waitCommandReply is')) -- server will either reply an OK packet
133+ ((\ _ -> return () ) :: SomeException -> IO () ) -- or directy close the connection
134+ conn = MySQLConn is'
135+ (write c)
136+ (writeCommand COM_QUIT (write c) >> waitNotMandatoryOK >> TCP. close c)
137+ consumed
138+ return (greet, conn)
139139
140140 connectWithBufferSize h p bs = TCP. connectSocket h p >>= TCP. socketToConnection bs
141141 write c a = TCP. send c $ Binary. runPut . Binary. put $ a
142142
143143mkAuth :: ByteString -> ByteString -> ByteString -> Word8 -> Greeting -> Auth
144144mkAuth db user pass charset greet =
145145 let salt = greetingSalt1 greet `B.append` greetingSalt2 greet
146- scambleBuf = scramble salt pass
147- in Auth clientCap clientMaxPacketSize charset user scambleBuf db
148- where
149- scramble :: ByteString -> ByteString -> ByteString
150- scramble salt pass'
151- | B. null pass' = B. empty
152- | otherwise = B. pack (B. zipWith xor sha1pass withSalt)
153- where sha1pass = sha1 pass'
154- withSalt = sha1 (salt `B.append` sha1 sha1pass)
155-
156- sha1 :: ByteString -> ByteString
157- sha1 = BA. convert . (Crypto. hash :: ByteString -> Crypto. Digest Crypto. SHA1 )
146+ plugin = greetingAuthPlugin greet
147+ scambleBuf = scrambleForPlugin plugin salt pass
148+ in Auth clientCap clientMaxPacketSize charset user scambleBuf db plugin
149+
150+ -- | Dispatch scramble based on the authentication plugin name.
151+ scrambleForPlugin :: ByteString -> ByteString -> ByteString -> ByteString
152+ scrambleForPlugin plugin salt pass
153+ | plugin == " caching_sha2_password" = scrambleSHA256 salt pass
154+ | otherwise = scrambleSHA1 salt pass
155+
156+ -- | SHA1-based scramble for @mysql_native_password@.
157+ scrambleSHA1 :: ByteString -> ByteString -> ByteString
158+ scrambleSHA1 salt pass
159+ | B. null pass = B. empty
160+ | otherwise = B. pack (B. zipWith xor sha1pass withSalt)
161+ where sha1pass = sha1 pass
162+ withSalt = sha1 (salt `B.append` sha1 sha1pass)
163+ sha1 :: ByteString -> ByteString
164+ sha1 = BA. convert . (Crypto. hash :: ByteString -> Crypto. Digest Crypto. SHA1 )
165+
166+ -- | SHA256-based scramble for @caching_sha2_password@.
167+ -- XOR(SHA256(password), SHA256(SHA256(SHA256(password)) + nonce))
168+ scrambleSHA256 :: ByteString -> ByteString -> ByteString
169+ scrambleSHA256 salt pass
170+ | B. null pass = B. empty
171+ | otherwise = B. pack (B. zipWith xor sha256pass withSalt)
172+ where sha256pass = sha256 pass
173+ withSalt = sha256 (sha256 sha256pass `B.append` salt)
174+ sha256 :: ByteString -> ByteString
175+ sha256 = BA. convert . (Crypto. hash :: ByteString -> Crypto. Digest Crypto. SHA256 )
176+
177+ -- | Handle multi-step authentication after sending the initial auth response.
178+ --
179+ -- This handles OK, ERR, AuthMoreData (0x01), and AuthSwitchRequest (0xFE).
180+ -- The @fullAuth@ callback is invoked when the server requests full authentication
181+ -- (e.g., cleartext password over TLS).
182+ completeAuth :: InputStream Packet -- ^ packet input stream
183+ -> (Packet -> IO () ) -- ^ packet writer
184+ -> ByteString -- ^ password
185+ -> Packet -- ^ the first response packet from server
186+ -> (Word8 -> ByteString -> (Packet -> IO () ) -> InputStream Packet -> IO () )
187+ -- ^ full auth callback (seqN, password, writer, input)
188+ -> IO ()
189+ completeAuth is writePacket pass p fullAuth
190+ | isOK p = return ()
191+ | isERR p = decodeFromPacket p >>= throwIO . ERRException
192+ | isAuthMoreData p = do
193+ let body = L. toStrict (pBody p)
194+ case B. index body 1 of
195+ 0x03 -> do -- fast auth success, read the final OK
196+ ok <- readPacket is
197+ if isOK ok
198+ then return ()
199+ else decodeFromPacket ok >>= throwIO . ERRException
200+ 0x04 -> do -- full auth required
201+ fullAuth (pSeqN p + 1 ) pass writePacket is
202+ _ -> throwIO (UnexpectedPacket p)
203+ | isAuthSwitch p = do
204+ -- Parse AuthSwitchRequest: 0xFE, plugin name (NUL), salt
205+ let body = L. toStrict (pBody p)
206+ rest = B. drop 1 body -- skip 0xFE
207+ (newPlugin, rest') = B. break (== 0 ) rest
208+ newSalt = B. drop 1 rest' -- skip NUL; trailing NUL may or may not be present
209+ -- Remove trailing NUL from salt if present
210+ newSalt' = if not (B. null newSalt) && B. last newSalt == 0
211+ then B. init newSalt
212+ else newSalt
213+ scrambled = scrambleForPlugin newPlugin newSalt' pass
214+ seqN = pSeqN p + 1
215+ responseBody = L. fromStrict scrambled
216+ responsePacket = Packet (fromIntegral (B. length scrambled)) seqN responseBody
217+ writePacket responsePacket
218+ q <- readPacket is
219+ completeAuth is writePacket pass q fullAuth
220+ | otherwise = throwIO (UnexpectedPacket p)
221+
222+ -- | Full auth handler for plain TCP connections: throws an error because
223+ -- caching_sha2_password full authentication requires a secure connection.
224+ plainFullAuth :: Word8 -> ByteString -> (Packet -> IO () ) -> InputStream Packet -> IO ()
225+ plainFullAuth _ _ _ _ =
226+ throwIO $ AuthException " caching_sha2_password full authentication requires a TLS connection. Use Database.MySQL.TLS to connect, or ensure the password verifier is cached (fast auth path)."
227+
228+ data AuthException = AuthException String deriving (Typeable , Show )
229+ instance Exception AuthException
158230
159231-- | A specialized 'decodeInputStream' here for speed
160232decodeInputStream :: InputStream ByteString -> IO (InputStream Packet )
0 commit comments