{-# OPTIONS_HADDOCK not-home #-} -- | -- Description : PAKE exchange for Magic Wormhole -- -- Once a peer is found, and both sides have mailboxes open, the peers need to -- generate a shared 'ClientProtocol.SessionKey' based on their shared password. -- -- 'pakeExchange' has the logic for doing this. module MagicWormhole.Internal.Pake ( pakeExchange , PakeError(..) -- * Exported for testing , spakeBytesToMessageBody , messageBodyToSpakeBytes ) where import Protolude 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 -- | Exchange SPAKE2 keys with a Magic Wormhole peer. -- -- Throws an 'Error' if we cannot parse the incoming message. pakeExchange :: ClientProtocol.Connection -- ^ A connection to a peer -> Spake2.Password -- ^ The shared password. Construct with 'Spake2.makePassword'. -> IO ClientProtocol.SessionKey -- ^ A key that can be used for the remainder of the session 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 -- This is kind of a fun approach, but it means that everyone else has -- to promise that they *don't* consume pake messages. msg <- ClientProtocol.receive conn unless (Messages.phase msg == Messages.PakePhase) retry pure $ messageBodyToSpakeBytes (Messages.body msg) -- | The message we send to negotiate the shared session key. 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 -- | Encode the bytes generated by the SPAKE2 algorithm into a Magic Wormhole -- message body. spakeBytesToMessageBody :: ByteString -> Messages.Body spakeBytesToMessageBody = Messages.Body . toS . Aeson.encode . Spake2Message -- | Decode a Magic Wormhole message body into bytes that can be used as input -- into the SPAKE2 algorithm. messageBodyToSpakeBytes :: Messages.Body -> Either Text ByteString messageBodyToSpakeBytes (Messages.Body bodyBytes) = bimap toS spake2Bytes . Aeson.eitherDecode . toS $ bodyBytes -- | Construct a SPAKE2 protocol compatible with Magic Wormhole. 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') -- | The version of the SPAKE2 protocol used by Magic Wormhole. type Spake2Protocol = Spake2.Protocol Ed25519 SHA256 -- | An error that occured during 'pakeExchange'. newtype PakeError = Error (Spake2.MessageError Text) deriving (Eq, Show, Typeable) instance Exception PakeError