Browse Source

Merge remote-tracking branch 'philonous/master'

master
Jon Kristensen 14 years ago
parent
commit
c5841a6325
  1. 2
      pontarius.cabal
  2. 4
      source/Network/Xmpp.hs
  3. 5
      source/Network/Xmpp/Monad.hs
  4. 50
      source/Network/Xmpp/Sasl.hs
  5. 171
      source/Network/Xmpp/Sasl/Common.hs
  6. 70
      source/Network/Xmpp/Sasl/DigestMD5.hs
  7. 52
      source/Network/Xmpp/Sasl/Plain.hs
  8. 68
      source/Network/Xmpp/Sasl/Sasl.hs
  9. 139
      source/Network/Xmpp/Sasl/Scram.hs
  10. 19
      source/Network/Xmpp/Sasl/Types.hs
  11. 17
      source/Network/Xmpp/Types.hs
  12. 10
      tests/Tests.hs

2
pontarius.cabal

@ -37,6 +37,7 @@ Library
, binary -any , binary -any
, attoparsec -any , attoparsec -any
, crypto-api -any , crypto-api -any
, cryptohash -any
, text -any , text -any
, bytestring -any , bytestring -any
, transformers -any , transformers -any
@ -62,6 +63,7 @@ Library
, Network.Xmpp.Sasl , Network.Xmpp.Sasl
, Network.Xmpp.Sasl.Plain , Network.Xmpp.Sasl.Plain
, Network.Xmpp.Sasl.DigestMD5 , Network.Xmpp.Sasl.DigestMD5
, Network.Xmpp.Sasl.Scram
, Network.Xmpp.Sasl.Types , Network.Xmpp.Sasl.Types
, Network.Xmpp.Session , Network.Xmpp.Session
, Network.Xmpp.Stream , Network.Xmpp.Stream

4
source/Network/Xmpp.hs

@ -157,6 +157,8 @@ import Network.Xmpp.Message
import Network.Xmpp.Monad import Network.Xmpp.Monad
import Network.Xmpp.Presence import Network.Xmpp.Presence
import Network.Xmpp.Sasl import Network.Xmpp.Sasl
import Network.Xmpp.Sasl.Scram
import Network.Xmpp.Sasl.Plain
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
import Network.Xmpp.Session import Network.Xmpp.Session
import Network.Xmpp.Stream import Network.Xmpp.Stream
@ -177,7 +179,7 @@ auth :: Text.Text -- ^ The username
-- assign one -- assign one
-> XmppConMonad (Either AuthError Text.Text) -> XmppConMonad (Either AuthError Text.Text)
auth username passwd resource = runErrorT $ do auth username passwd resource = runErrorT $ do
ErrorT $ xmppSASL [DIGEST_MD5Credentials Nothing username passwd] ErrorT $ xmppSasl username Nothing [scramSha1 $ return passwd]
res <- lift $ xmppBind resource res <- lift $ xmppBind resource
lift $ xmppStartSession lift $ xmppStartSession
return res return res

5
source/Network/Xmpp/Monad.hs

@ -99,6 +99,7 @@ xmppNoConnection = XmppConnection
, sFeatures = SF Nothing [] [] , sFeatures = SF Nothing [] []
, sConnectionState = XmppConnectionClosed , sConnectionState = XmppConnectionClosed
, sHostname = Nothing , sHostname = Nothing
, sAuthzid = Nothing
, sUsername = Nothing , sUsername = Nothing
, sResource = Nothing , sResource = Nothing
, sCloseConnection = return () , sCloseConnection = return ()
@ -111,7 +112,6 @@ xmppNoConnection = XmppConnection
-- updates the XmppConMonad XmppConnection state. -- updates the XmppConMonad XmppConnection state.
xmppRawConnect :: HostName -> Text -> XmppConMonad () xmppRawConnect :: HostName -> Text -> XmppConMonad ()
xmppRawConnect host hostname = do xmppRawConnect host hostname = do
uname <- gets sUsername
con <- liftIO $ do con <- liftIO $ do
con <- connectTo host (PortNumber 5222) con <- connectTo host (PortNumber 5222)
hSetBuffering con NoBuffering hSetBuffering con NoBuffering
@ -126,7 +126,8 @@ xmppRawConnect host hostname = do
(SF Nothing [] []) (SF Nothing [] [])
XmppConnectionPlain XmppConnectionPlain
(Just hostname) (Just hostname)
uname Nothing
Nothing
Nothing Nothing
(hClose con) (hClose con)
put st put st

50
source/Network/Xmpp/Sasl.hs

@ -30,38 +30,42 @@ import Network.Xmpp.Pickle
import qualified System.Random as Random import qualified System.Random as Random
import Network.Xmpp.Sasl.Sasl
import Network.Xmpp.Sasl.DigestMD5 import Network.Xmpp.Sasl.DigestMD5
import Network.Xmpp.Sasl.Plain import Network.Xmpp.Sasl.Plain
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
runSasl :: (Text.Text -> Text.Text -> Maybe Text.Text -> SaslM a)
-> Text.Text
-> Maybe Text.Text
-> XmppConMonad (Either AuthError a)
runSasl authAction authcid authzid = runErrorT $ do
hn <- gets sHostname
case hn of
Just hn' -> do
r <- authAction hn' authcid authzid
modify (\s -> s{ sUsername = Just authcid
, sAuthzid = authzid
})
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream
return r
Nothing -> throwError AuthConnectionError
-- Uses the first supported mechanism to authenticate, if any. Updates the -- Uses the first supported mechanism to authenticate, if any. Updates the
-- XmppConMonad state with non-password credentials and restarts the stream upon -- XmppConMonad state with non-password credentials and restarts the stream upon
-- success. This computation wraps an ErrorT computation, which means that -- success. This computation wraps an ErrorT computation, which means that
-- catchError can be used to catch any errors. -- catchError can be used to catch any errors.
xmppSASL :: [SASLCredentials] -- ^ Acceptable authentication mechanisms and xmppSasl :: Text.Text
-- their corresponding credentials -> Maybe Text.Text
-> [SaslHandler] -- ^ Acceptable authentication
-- mechanisms and their corresponding
-- handlers
-> XmppConMonad (Either AuthError ()) -> XmppConMonad (Either AuthError ())
xmppSASL creds = runErrorT $ do xmppSasl authcid authzid handlers = do
-- Chooses the first mechanism that is acceptable by both the client and the -- Chooses the first mechanism that is acceptable by both the client and the
-- server. -- server.
mechanisms <- gets $ saslMechanisms . sFeatures mechanisms <- gets $ saslMechanisms . sFeatures
let cred = L.find (\cred -> credsToName cred `elem` mechanisms) creds case (filter (\(name,_) -> name `elem` mechanisms)) handlers of
unless (isJust cred) (throwError $ AuthMechanismError mechanisms) [] -> return . Left $ AuthNoAcceptableMechanism mechanisms
case fromJust cred of (_name, handler):_ -> runSasl handler authcid authzid
DIGEST_MD5Credentials authzid authcid passwd -> ErrorT $ xmppDigestMD5
authzid
authcid
passwd
PLAINCredentials authzid authcid passwd -> ErrorT $ xmppPLAIN
authzid
authcid
passwd
_ -> error "xmppSASL: Mechanism not caught"
where
-- Converts the credentials to the appropriate mechanism name, corresponding to
-- the XMPP mechanism attribute.
credsToName :: SASLCredentials -> Text
credsToName (DIGEST_MD5Credentials _ _ _) = "DIGEST-MD5"
credsToName (PLAINCredentials _ _ _) = "PLAIN"
credsToName c = error $ "credsToName failed for " ++ (show c)

171
source/Network/Xmpp/Sasl/Common.hs

@ -0,0 +1,171 @@
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Sasl.Common where
import Network.Xmpp.Types
import Control.Applicative ((<$>))
import Control.Monad.Error
import Control.Monad.State.Class
import qualified Data.Attoparsec.ByteString.Char8 as AP
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.Maybe (fromMaybe)
import Data.Maybe (maybeToList)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.XML.Pickle
import Data.XML.Types
import Data.Word (Word8)
import Network.Xmpp.Monad
import Network.Xmpp.Pickle
import Network.Xmpp.Sasl.Types
import qualified System.Random as Random
data SaslElement = SaslSuccess (Maybe Text.Text)
| SaslChallenge (Maybe Text.Text)
--makeNonce :: SaslM BS.ByteString
makeNonce :: IO BS.ByteString
makeNonce = do
g <- liftIO Random.newStdGen
return $ B64.encode . BS.pack . map toWord8 . take 15 $ Random.randoms g
where
toWord8 :: Int -> Word8
toWord8 x = fromIntegral x :: Word8
-- The <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> element, with an
-- optional round-trip value.
saslInitE :: Text.Text -> Maybe Text.Text -> Element
saslInitE mechanism rt =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}auth"
[("mechanism", [ContentText mechanism])]
(maybeToList $ NodeContent . ContentText <$> rt)
-- SASL response with text payload.
saslResponseE :: Maybe Text.Text -> Element
saslResponseE resp =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response"
[]
(maybeToList $ NodeContent . ContentText <$> resp)
xpSuccess :: PU [Node] (Maybe Text.Text)
xpSuccess = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}success"
(xpOption $ xpContent xpId)
-- Parses the incoming SASL data to a mapped list of pairs.
pairs :: BS.ByteString -> Either String Pairs
pairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do
AP.skipSpace
name <- AP.takeWhile1 (/= '=')
_ <- AP.char '='
quote <- ((AP.char '"' >> return True) `mplus` return False)
content <- AP.takeWhile1 (AP.notInClass [',', '"'])
when quote . void $ AP.char '"'
return (name, content)
-- Failure element pickler.
xpFailure :: PU [Node] SaslFailure
xpFailure = xpWrap
(\(txt, (failure, _, _)) -> SaslFailure failure txt)
(\(SaslFailure failure txt) -> (txt,(failure,(),())))
(xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}failure"
(xp2Tuple
(xpOption $ xpElem
"{urn:ietf:params:xml:ns:xmpp-sasl}text"
xpLangTag
(xpContent xpId))
(xpElemByNamespace
"urn:ietf:params:xml:ns:xmpp-sasl"
xpPrim
(xpUnit)
(xpUnit))))
-- Challenge element pickler.
xpChallenge :: PU [Node] (Maybe Text.Text)
xpChallenge = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge"
(xpOption $ xpContent xpId)
-- | pickler for SaslElement
xpSaslElement :: PU [Node] SaslElement
xpSaslElement = xpAlt saslSel
[ xpWrap SaslSuccess (\(SaslSuccess x) -> x) xpSuccess
, xpWrap SaslChallenge (\(SaslChallenge c) -> c) xpChallenge
]
where
saslSel (SaslSuccess _) = 0
saslSel (SaslChallenge _) = 1
-- | Add quotationmarks around a byte string
quote :: BS.ByteString -> BS.ByteString
quote x = BS.concat ["\"",x,"\""]
saslInit :: Text.Text -> Maybe BS.ByteString -> SaslM Bool
saslInit mechanism payload = lift . pushElement . saslInitE mechanism $
Text.decodeUtf8 . B64.encode <$> payload
-- | Pull the next element
pullSaslElement :: SaslM SaslElement
pullSaslElement = do
el <- lift $ pullPickle (xpEither xpFailure xpSaslElement)
case el of
Left e ->throwError $ AuthSaslFailure e
Right r -> return r
-- | Pull the next element, checking that it is a challenge
pullChallenge :: SaslM (Maybe BS.ByteString)
pullChallenge = do
e <- pullSaslElement
case e of
SaslChallenge Nothing -> return Nothing
SaslChallenge (Just scb64)
| Right sc <- B64.decode . Text.encodeUtf8 $ scb64
-> return $ Just sc
_ -> throwError AuthChallengeError
-- | Extract value from Just, failing with AuthChallengeError on Nothing
saslFromJust :: Maybe a -> SaslM a
saslFromJust Nothing = throwError $ AuthChallengeError
saslFromJust (Just d) = return d
-- | Pull the next element and check that it is success
pullSuccess :: SaslM (Maybe Text.Text)
pullSuccess = do
e <- pullSaslElement
case e of
SaslSuccess x -> return x
_ -> throwError $ AuthXmlError
-- | Pull the next element. When it's success, return it's payload.
-- If it's a challenge, send an empty response and pull success
pullFinalMessage :: SaslM (Maybe BS.ByteString)
pullFinalMessage = do
challenge2 <- pullSaslElement
case challenge2 of
SaslSuccess x -> decode x
SaslChallenge x -> do
_b <- respond Nothing
_s <- pullSuccess
decode x
where
decode Nothing = return Nothing
decode (Just d) = case B64.decode $ Text.encodeUtf8 d of
Left _e -> throwError $ AuthChallengeError
Right x -> return $ Just x
-- | Extract p=q pairs from a challenge
toPairs :: BS.ByteString -> SaslM Pairs
toPairs ctext = case pairs ctext of
Left _e -> throwError AuthChallengeError
Right r -> return r
-- | Send a SASL response element. The content will be base64-encoded for you
respond :: Maybe BS.ByteString -> SaslM Bool
respond = lift . pushElement . saslResponseE .
fmap (Text.decodeUtf8 . B64.encode)

70
source/Network/Xmpp/Sasl/DigestMD5.hs

@ -17,7 +17,6 @@ import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.Digest.Pure.MD5 as MD5 import qualified Data.Digest.Pure.MD5 as MD5
import qualified Data.List as L import qualified Data.List as L
import Data.Word (Word8)
import qualified Data.Text as Text import qualified Data.Text as Text
import Data.Text (Text) import Data.Text (Text)
@ -34,68 +33,47 @@ import Network.Xmpp.Stream
import Network.Xmpp.Types import Network.Xmpp.Types
import Network.Xmpp.Pickle import Network.Xmpp.Pickle
import qualified System.Random as Random
import Network.Xmpp.Sasl.Sasl import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
xmppDigestMD5 :: Maybe Text -- Authorization identity (authzid) xmppDigestMD5 :: Maybe Text -- Authorization identity (authzid)
-> Text -- Authentication identity (authzid) -> Text -- Authentication identity (authzid)
-> Text -- Password (authzid) -> Text -- Password (authzid)
-> XmppConMonad (Either AuthError ()) -> XmppConMonad (Either AuthError ())
xmppDigestMD5 authzid authcid passwd = runErrorT $ do xmppDigestMD5 authzid authcid passwd = runErrorT $ do
realm <- gets sHostname hn <- gets sHostname
case realm of case hn of
Just realm' -> do Just hn' -> do
ErrorT $ xmppDIGEST_MD5' realm' xmppDigestMD5' hn'
-- TODO: Save authzid -- TODO: Save authzid
modify (\s -> s{sUsername = Just authcid}) modify (\s -> s{sUsername = Just authcid})
Nothing -> throwError AuthConnectionError Nothing -> throwError AuthConnectionError
where where
xmppDIGEST_MD5' :: Text -- ^ SASL realm xmppDigestMD5' :: Text -> SaslM ()
-> XmppConMonad (Either AuthError ()) xmppDigestMD5' hostname = do
xmppDIGEST_MD5' realm = runErrorT $ do -- Push element and receive the challenge (in SaslM).
-- Push element and receive the challenge (in XmppConMonad). _ <- saslInit "DIGEST-MD5" Nothing -- TODO: Check boolean?
_ <- lift . pushElement $ saslInitE "DIGEST-MD5" Nothing -- TODO: Check boolean? pairs <- toPairs =<< saslFromJust =<< pullChallenge
challenge' <- lift $ B64.decode . Text.encodeUtf8 <$> cnonce <- liftIO $ makeNonce
pullPickle challengePickle _b <- respond . Just $ createResponse hostname pairs cnonce
challenge <- case challenge' of challenge2 <- pullFinalMessage
Left _e -> throwError AuthChallengeError
Right r -> return r
pairs <- case toPairs challenge of
Left _ -> throwError AuthChallengeError
Right p -> return p
g <- liftIO Random.newStdGen
_ <- lift . pushElement . -- TODO: Check boolean?
saslResponseE $ createResponse g realm pairs
challenge2 <- lift $ pullPickle (xpEither failurePickle challengePickle)
case challenge2 of
Left _x -> throwError AuthXmlError
Right _ -> return ()
lift $ pushElement saslResponse2E
e <- lift pullElement
case e of
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] ->
return ()
_ -> throwError AuthXmlError -- TODO: investigate
-- The SASL authentication has succeeded; the stream is restarted.
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream _ <- ErrorT $ left AuthStreamError <$> xmppRestartStream
return () return ()
-- Produce the response to the challenge. -- Produce the response to the challenge.
createResponse :: Random.RandomGen g createResponse :: Text
=> g -> Pairs
-> Text -> BS.ByteString -- nonce
-> [(BS8.ByteString, BS8.ByteString)] -- Pairs -> BS.ByteString
-> Text createResponse hostname pairs cnonce = let
createResponse g hostname pairs = let
Just qop = L.lookup "qop" pairs Just qop = L.lookup "qop" pairs
Just nonce = L.lookup "nonce" pairs Just nonce = L.lookup "nonce" pairs
uname_ = Text.encodeUtf8 authcid uname_ = Text.encodeUtf8 authcid
passwd_ = Text.encodeUtf8 passwd passwd_ = Text.encodeUtf8 passwd
-- Using Int instead of Word8 for random 1.0.0.0 (GHC 7) compatibility. -- Using Int instead of Word8 for random 1.0.0.0 (GHC 7) compatibility.
cnonce = BS.tail . BS.init .
B64.encode . BS.pack . map toWord8 .
take 8 $ Random.randoms g
nc = "00000001" nc = "00000001"
digestURI = "xmpp/" `BS.append` Text.encodeUtf8 hostname digestURI = "xmpp/" `BS.append` Text.encodeUtf8 hostname
digest = md5Digest digest = md5Digest
@ -120,11 +98,7 @@ xmppDigestMD5 authzid authcid passwd = runErrorT $ do
, ["response" , digest ] , ["response" , digest ]
, ["charset" , "utf-8" ] , ["charset" , "utf-8" ]
] ]
in Text.decodeUtf8 $ B64.encode response in B64.encode response
quote :: BS8.ByteString -> BS8.ByteString
quote x = BS.concat ["\"",x,"\""]
toWord8 :: Int -> Word8
toWord8 x = fromIntegral x :: Word8
hash :: [BS8.ByteString] -> BS8.ByteString hash :: [BS8.ByteString] -> BS8.ByteString
hash = BS8.pack . show hash = BS8.pack . show
. (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":") . (CC.hash' :: BS.ByteString -> MD5.MD5Digest) . BS.intercalate (":")

52
source/Network/Xmpp/Sasl/Plain.hs

@ -40,34 +40,38 @@ import Network.Xmpp.Pickle
import qualified System.Random as Random import qualified System.Random as Random
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import qualified Data.Text as T import qualified Data.Text as Text
import Network.Xmpp.Sasl.Sasl import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types import Network.Xmpp.Sasl.Types
xmppPLAIN :: Maybe T.Text -- TODO: stringprep
-> T.Text xmppPlain :: SaslM Text.Text
-> T.Text -> a
-> XmppConMonad (Either AuthError ()) -> Text.Text
xmppPLAIN authzid authcid passwd = runErrorT $ do -> Maybe Text.Text
_ <- lift . pushElement $ saslInitE "PLAIN" $ -- TODO: Check boolean? -> SaslM ()
Just $ Text.decodeUtf8 $ B64.encode $ Text.encodeUtf8 $ plainMessage authzid authcid passwd xmppPlain pw _hostname authcid authzid = do
lift $ pushElement saslResponse2E passwd <- pw
e <- lift pullElement _ <- saslInit "PLAIN" ( Just $ plainMessage authzid authcid passwd)
case e of _ <- pullSuccess
Element "{urn:ietf:params:xml:ns:xmpp-sasl}success" [] [] ->
return ()
_ -> throwError AuthXmlError -- TODO: investigate
-- The SASL authentication has succeeded; the stream is restarted.
_ <- ErrorT $ left AuthStreamError <$> xmppRestartStream
return () return ()
where where
-- Converts an optional authorization identity, an authentication identity, -- Converts an optional authorization identity, an authentication identity,
-- and a password to a \NUL-separated PLAIN message. -- and a password to a \NUL-separated PLAIN message.
plainMessage :: Maybe T.Text -- Authorization identity (authzid) plainMessage :: Maybe Text.Text -- Authorization identity (authzid)
-> T.Text -- Authentication identity (authcid) -> Text.Text -- Authentication identity (authcid)
-> T.Text -- Password -> Text.Text -- Password
-> T.Text -- The PLAIN message -> BS.ByteString -- The PLAIN message
plainMessage authzid authcid passwd = plainMessage authzid authcid passwd = BS.concat $
let authzid' = fromMaybe "" authzid in [ authzid'
T.concat [authzid', "\NUL", authcid, "\NUL", passwd] , "\NUL"
, Text.encodeUtf8 $ authcid
, "\NUL"
, Text.encodeUtf8 $ passwd
]
where
authzid' = maybe "" Text.encodeUtf8 authzid
plain :: SaslM Text.Text -> SaslHandler
plain passwd = ("PLAIN", xmppPlain passwd)

68
source/Network/Xmpp/Sasl/Sasl.hs

@ -1,68 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Sasl.Sasl where
import Network.Xmpp.Types
import Control.Monad.Error
import Data.Text
import qualified Data.Attoparsec.ByteString.Char8 as AP
import Data.XML.Pickle
import Data.XML.Types
import qualified Data.ByteString as BS
import Data.Maybe (fromMaybe)
import Network.Xmpp.Pickle
-- The <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> element, with an
-- optional round-trip value.
saslInitE :: Text -> Maybe Text -> Element
saslInitE mechanism rt =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}auth"
[("mechanism", [ContentText mechanism])]
[NodeContent $ ContentText $ fromMaybe "" rt]
-- SASL response with text payload.
saslResponseE :: Text -> Element
saslResponseE resp =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response"
[]
[NodeContent $ ContentText resp]
-- SASL response without payload.
saslResponse2E :: Element
saslResponse2E =
Element "{urn:ietf:params:xml:ns:xmpp-sasl}response"
[]
[]
-- Parses the incoming SASL data to a mapped list of pairs.
toPairs :: BS.ByteString -> Either String [(BS.ByteString, BS.ByteString)]
toPairs = AP.parseOnly . flip AP.sepBy1 (void $ AP.char ',') $ do
AP.skipSpace
name <- AP.takeWhile1 (/= '=')
_ <- AP.char '='
quote <- ((AP.char '"' >> return True) `mplus` return False)
content <- AP.takeWhile1 (AP.notInClass [',', '"'])
when quote . void $ AP.char '"'
return (name, content)
-- Failure element pickler.
failurePickle :: PU [Node] SaslFailure
failurePickle = xpWrap
(\(txt, (failure, _, _)) -> SaslFailure failure txt)
(\(SaslFailure failure txt) -> (txt,(failure,(),())))
(xpElemNodes
"{urn:ietf:params:xml:ns:xmpp-sasl}failure"
(xp2Tuple
(xpOption $ xpElem
"{urn:ietf:params:xml:ns:xmpp-sasl}text"
xpLangTag
(xpContent xpId))
(xpElemByNamespace
"urn:ietf:params:xml:ns:xmpp-sasl"
xpPrim
(xpUnit)
(xpUnit))))
-- Challenge element pickler.
challengePickle :: PU [Node] Text
challengePickle = xpElemNodes "{urn:ietf:params:xml:ns:xmpp-sasl}challenge"
(xpIsolate $ xpContent xpId)

139
source/Network/Xmpp/Sasl/Scram.hs

@ -0,0 +1,139 @@
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Xmpp.Sasl.Scram where
import Control.Applicative((<$>))
import Control.Monad.Error
import Control.Monad.Trans (liftIO)
import qualified Crypto.Classes as Crypto
import qualified Crypto.HMAC as Crypto
import qualified Crypto.Hash.SHA1 as Crypto
import Data.Binary(Binary,encode)
import Data.Bits
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 as BS8 (unpack)
import qualified Data.ByteString.Lazy as LBS
import Data.List (foldl1')
import qualified Data.Binary.Builder as Build
import Data.Maybe (maybeToList)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Word(Word8)
import Network.Xmpp.Sasl.Common
import Network.Xmpp.Sasl.Types
-- | Bit-wise xor of byte strings
xorBS :: BS.ByteString -> BS.ByteString -> BS.ByteString
xorBS x y = BS.pack $ BS.zipWith xor x y
-- | Join byte strings with ","
merge :: [BS.ByteString] -> BS.ByteString
merge = BS.intercalate ","
-- | Infix concatenation of byte strings
(+++) :: BS.ByteString -> BS.ByteString -> BS.ByteString
(+++) = BS.append
-- | A nicer name for undefined, for use as a dummy token to determin
-- the hash function to use
hashToken :: (Crypto.Hash ctx hash) => hash
hashToken = undefined
-- | Salted Challenge Response Authentication Mechanism (SCRAM) SASL
-- mechanism according to RFC 5802.
--
-- This implementation is independent and polymorphic in the used hash function.
scram :: (Crypto.Hash ctx hash)
=> hash -- ^ Dummy argument to determine the hash to use. You
-- can safely pass undefined or a 'hashToken' to it
-> Text.Text -- ^ authentication ID (username)
-> Maybe Text.Text -- ^ authorization ID
-> Text.Text -- ^ password
-> SaslM ()
scram hashToken authcid authzid' password = do
cnonce <- liftIO $ makeNonce
saslInit "SCRAM-SHA-1" (Just $ cFirstMessage cnonce)
liftIO $ putStrLn "pulling challenge"
sFirstMessage <- saslFromJust =<< pullChallenge
liftIO $ putStrLn "pulled challenge"
pairs <- toPairs sFirstMessage
(nonce, salt, ic) <- fromPairs pairs cnonce
let (cfm, v) = cFinalMessageAndVerifier nonce salt ic sFirstMessage cnonce
respond $ Just cfm
finalPairs <- toPairs =<< saslFromJust =<< pullFinalMessage
unless (lookup "v" finalPairs == Just v) $ throwError AuthServerAuthError
return ()
where
-- We need to jump through some hoops to get a polymorphic solution
encode :: Crypto.Hash ctx hash => hash -> hash -> BS.ByteString
encode _hashtoken = Crypto.encode
hash str = encode hashToken $ Crypto.hash' str
hmac key str = encode hashToken $ Crypto.hmac' (Crypto.MacKey key) str
authzid = (\z -> "a=" +++ normalize z) <$> authzid'
gs2CbindFlag = "n" -- we don't support channel binding yet
gs2Header = merge $ [ gs2CbindFlag
, maybe "" id authzid
, ""
]
cbindData = "" -- we don't support channel binding yet
cFirstMessageBare cnonce = merge [ "n=" +++ normalize authcid
, "r=" +++ cnonce]
cFirstMessage cnonce = gs2Header +++ cFirstMessageBare cnonce
fromPairs :: Pairs
-> BS.ByteString
-> SaslM (BS.ByteString, BS.ByteString, Int)
fromPairs pairs cnonce | Just nonce <- lookup "r" pairs
, cnonce `BS.isPrefixOf` nonce
, Just salt' <- lookup "s" pairs
, Right salt <- B64.decode salt'
, Just ic <- lookup "i" pairs
, [(i,"")] <- reads $ BS8.unpack ic
= return (nonce, salt, i :: Int)
fromPairs _ _ = throwError $ AuthChallengeError
cFinalMessageAndVerifier nonce salt ic sfm cnonce
= (merge [ cFinalMessageWOProof
, "p=" +++ B64.encode clientProof
]
, B64.encode serverSignature
)
where
cFinalMessageWOProof = merge [ "c=" +++ B64.encode gs2Header
, "r=" +++ nonce]
saltedPassword = hi (normalize password) salt ic
clientKey = hmac saltedPassword "Client Key"
storedKey = hash clientKey
authMessage = merge [ cFirstMessageBare cnonce
, sfm
, cFinalMessageWOProof
]
clientSignature = hmac storedKey authMessage
clientProof = clientKey `xorBS` clientSignature
serverKey = hmac saltedPassword "Server Key"
serverSignature = hmac serverKey authMessage
-- helper
hi str salt ic = foldl1' xorBS (take ic us)
where
u1 = hmac str (salt +++ (BS.pack [0,0,0,1]))
us = iterate (hmac str) u1
normalize = Text.encodeUtf8 . id -- TODO: stringprep
base64 = B64.encode
-- | 'scram' spezialised to the SHA-1 hash function, packaged as a SaslHandler
scramSha1 :: SaslM Text.Text -> SaslHandler
scramSha1 passwd = ("SCRAM-SHA-1"
, \_hostname authcid authzid -> do
pw <- passwd
scram (hashToken :: Crypto.SHA1) authcid authzid pw
)

19
source/Network/Xmpp/Sasl/Types.hs

@ -1,16 +1,31 @@
module Network.Xmpp.Sasl.Types where module Network.Xmpp.Sasl.Types where
import Control.Monad.Error import Control.Monad.Error
import Data.Text import Control.Monad.State.Strict
import Data.ByteString(ByteString)
import qualified Data.Text as Text
import Network.Xmpp.Types import Network.Xmpp.Types
data AuthError = AuthXmlError data AuthError = AuthXmlError
| AuthMechanismError [Text] -- ^ Wraps mechanisms offered | AuthNoAcceptableMechanism [Text.Text] -- ^ Wraps mechanisms
-- offered
| AuthChallengeError | AuthChallengeError
| AuthServerAuthError -- ^ The server failed to authenticate
-- himself
| AuthStreamError StreamError -- ^ Stream error on stream restart | AuthStreamError StreamError -- ^ Stream error on stream restart
| AuthConnectionError -- ^ No host name set in state | AuthConnectionError -- ^ No host name set in state
| AuthError -- General instance used for the Error instance | AuthError -- General instance used for the Error instance
| AuthSaslFailure SaslFailure -- ^ defined SASL error condition
deriving Show deriving Show
instance Error AuthError where instance Error AuthError where
noMsg = AuthError noMsg = AuthError
type SaslM a = ErrorT AuthError (StateT XmppConnection IO) a
type Pairs = [(ByteString, ByteString)]
type SaslHandler = (Text.Text, Text.Text
-> Text.Text
-> Maybe Text.Text
-> SaslM ())

17
source/Network/Xmpp/Types.hs

@ -22,8 +22,8 @@ module Network.Xmpp.Types
, PresenceType(..) , PresenceType(..)
, SaslError(..) , SaslError(..)
, SaslFailure(..) , SaslFailure(..)
, SASLMechanism (..) , SaslMechanism (..)
, SASLCredentials (..) , SaslCredentials (..)
, ServerFeatures(..) , ServerFeatures(..)
, Stanza(..) , Stanza(..)
, StanzaError(..) , StanzaError(..)
@ -402,18 +402,18 @@ instance Read StanzaErrorCondition where
-- OTHER STUFF -- OTHER STUFF
-- ============================================================================= -- =============================================================================
data SASLCredentials = DIGEST_MD5Credentials (Maybe Text) Text Text data SaslCredentials = DigestMD5Credentials (Maybe Text) Text Text
| PLAINCredentials (Maybe Text) Text Text | PlainCredentials (Maybe Text) Text Text
instance Show SASLCredentials where instance Show SaslCredentials where
show (DIGEST_MD5Credentials authzid authcid _) = "DIGEST_MD5Credentials " ++ show (DigestMD5Credentials authzid authcid _) = "DIGEST_MD5Credentials " ++
(Text.unpack $ fromMaybe "" authzid) ++ " " ++ (Text.unpack authcid) ++ (Text.unpack $ fromMaybe "" authzid) ++ " " ++ (Text.unpack authcid) ++
" (password hidden)" " (password hidden)"
show (PLAINCredentials authzid authcid _) = "PLAINCredentials " ++ show (PlainCredentials authzid authcid _) = "PLAINCredentials " ++
(Text.unpack $ fromMaybe "" authzid) ++ " " ++ (Text.unpack authcid) ++ (Text.unpack $ fromMaybe "" authzid) ++ " " ++ (Text.unpack authcid) ++
" (password hidden)" " (password hidden)"
data SASLMechanism = DIGEST_MD5 deriving Show data SaslMechanism = DigestMD5 deriving Show
data SaslFailure = SaslFailure { saslFailureCondition :: SaslError data SaslFailure = SaslFailure { saslFailureCondition :: SaslError
, saslFailureText :: Maybe ( Maybe LangTag , saslFailureText :: Maybe ( Maybe LangTag
@ -655,6 +655,7 @@ data XmppConnection = XmppConnection
, sConnectionState :: XmppConnectionState , sConnectionState :: XmppConnectionState
, sHostname :: Maybe Text , sHostname :: Maybe Text
, sUsername :: Maybe Text , sUsername :: Maybe Text
, sAuthzid :: Maybe Text
, sResource :: Maybe Text , sResource :: Maybe Text
, sCloseConnection :: IO () , sCloseConnection :: IO ()
-- TODO: add default Language -- TODO: add default Language

10
tests/Tests.hs

@ -12,9 +12,9 @@ import qualified Data.Text as Text
import Data.XML.Pickle import Data.XML.Pickle
import Data.XML.Types import Data.XML.Types
import Network.XMPP import Network.Xmpp
import Network.XMPP.IM.Presence import Network.Xmpp.IM.Presence
import Network.XMPP.Pickle import Network.Xmpp.Pickle
import System.Environment import System.Environment
import Text.XML.Stream.Elements import Text.XML.Stream.Elements
@ -29,7 +29,7 @@ supervisor :: JID
supervisor = read "uart14@species64739.dyndns.org" supervisor = read "uart14@species64739.dyndns.org"
attXmpp :: STM a -> XMPP a attXmpp :: STM a -> Xmpp a
attXmpp = liftIO . atomically attXmpp = liftIO . atomically
testNS :: Text testNS :: Text
@ -70,7 +70,7 @@ iqResponder = do
liftIO $ threadDelay 1000000 liftIO $ threadDelay 1000000
endSession endSession
autoAccept :: XMPP () autoAccept :: Xmpp ()
autoAccept = forever $ do autoAccept = forever $ do
st <- waitForPresence isPresenceSubscribe st <- waitForPresence isPresenceSubscribe
sendPresence $ presenceSubscribed (fromJust $ presenceFrom st) sendPresence $ presenceSubscribed (fromJust $ presenceFrom st)

Loading…
Cancel
Save