{-# OPTIONS_HADDOCK not-home #-}
module MagicWormhole.Internal.Pake
( pakeExchange
, PakeError(..)
, spakeBytesToMessageBody
, messageBodyToSpakeBytes
) where
import Protolude hiding (toS)
import Protolude.Conv (toS)
import Control.Monad (fail)
import Crypto.Hash (SHA256(..))
import qualified Crypto.Spake2 as Spake2
import Crypto.Spake2.Group (Group(arbitraryElement))
import Crypto.Spake2.Groups (Ed25519(..))
import qualified Data.Aeson as Aeson
import Data.Aeson (FromJSON, ToJSON, (.=), object, Value(..), (.:))
import Data.Aeson.Types (typeMismatch)
import Data.ByteArray.Encoding (convertToBase, convertFromBase, Base(Base16))
import qualified MagicWormhole.Internal.Messages as Messages
import qualified MagicWormhole.Internal.ClientProtocol as ClientProtocol
pakeExchange
:: ClientProtocol.Connection
-> Spake2.Password
-> IO ClientProtocol.SessionKey
pakeExchange conn password = do
let protocol = wormholeSpakeProtocol (ClientProtocol.appID conn)
result <- Spake2.spake2Exchange protocol password sendPakeMessage (atomically receivePakeMessage)
case result of
Left err -> throwIO (Error err)
Right key -> pure (ClientProtocol.SessionKey key)
where
sendPakeMessage = ClientProtocol.send conn Messages.PakePhase . spakeBytesToMessageBody
receivePakeMessage = do
msg <- ClientProtocol.receive conn
unless (Messages.phase msg == Messages.PakePhase) retry
pure $ messageBodyToSpakeBytes (Messages.body msg)
newtype Spake2Message = Spake2Message { spake2Bytes :: ByteString } deriving (Eq, Show)
instance ToJSON Spake2Message where
toJSON (Spake2Message msg) = object [ "pake_v1" .= toS @ByteString @Text (convertToBase Base16 msg) ]
instance FromJSON Spake2Message where
parseJSON (Object msg) = do
hexKey <- toS @Text @ByteString <$> msg .: "pake_v1"
case convertFromBase Base16 hexKey of
Left err -> fail err
Right key -> pure $ Spake2Message key
parseJSON unknown = typeMismatch "Spake2Message" unknown
spakeBytesToMessageBody :: ByteString -> Messages.Body
spakeBytesToMessageBody = Messages.Body . toS . Aeson.encode . Spake2Message
messageBodyToSpakeBytes :: Messages.Body -> Either Text ByteString
messageBodyToSpakeBytes (Messages.Body bodyBytes) =
bimap toS spake2Bytes . Aeson.eitherDecode . toS $ bodyBytes
wormholeSpakeProtocol :: Messages.AppID -> Spake2Protocol
wormholeSpakeProtocol (Messages.AppID appID') =
Spake2.makeSymmetricProtocol SHA256 Ed25519 blind sideID
where
blind = arbitraryElement Ed25519 ("symmetric" :: ByteString)
sideID = Spake2.SideID (toS appID')
type Spake2Protocol = Spake2.Protocol Ed25519 SHA256
newtype PakeError = Error (Spake2.MessageError Text) deriving (Eq, Show, Typeable)
instance Exception PakeError