packages feed

commsec-0.2: Network/CommSec.hs

{-# LANGUAGE RecordWildCards, RankNTypes #-}
module Network.CommSec
    (
    -- * Types
      Connection(..), Safe, Unsafe
    -- * Send and receive operations
    , send, recv
    , sendPtr, recvPtr
    , sendUnsafe, recvUnsafe
    , sendPtrUnsafe, recvPtrUnsafe
    -- * Establishing a connection from a shared secret
    -- ** Thread Safe
    , accept
    , connect
    -- ** Non-threadsafe
    , acceptUnsafe
    , connectUnsafe
    -- * Establishing a connection from a public identity (PKI)
    -- , acceptId
    -- , connectId
    -- * Re-exports
    , SocketType(..)
    ) where

import Crypto.Classes (buildKey)
import Crypto.Cipher.AES128.Internal (encryptCTR)
import Crypto.Cipher.AES128 (AESKey)
import Network.CommSec.Package
import Network.Socket (Socket, SocketType(..), sendBuf, recvBuf, HostName, PortNumber)
import qualified Network.Socket as Net
import Data.IORef
import Control.Concurrent.MVar
import Control.Exception (throw)
import qualified Data.ByteString as B
import qualified Data.ByteString.Unsafe as B
import qualified Data.ByteString.Internal as B
import Foreign.Ptr
import Foreign.Marshal.Alloc
import Data.Word

-- | Unsafe connections are not thread safe.  Concurrent use can compromise
-- security.
type Unsafe = IORef

-- | Safe connections are more expensive due to the use of an 'MVar' but,
-- unlike their 'Unsafe' counterparts, Safe connections can be used
-- concurrently.
type Safe   = MVar

-- | A connection is a secure bidirectional communication channel.
data Connection c
            = Conn { inCtx        :: c InContext
                   , outCtx       :: c OutContext
                   , socket       :: Socket
                   }

send :: Connection Safe -> B.ByteString -> IO ()
send = sendWith takeMVar putMVar

recv :: Connection Safe -> IO B.ByteString
recv = recvWith takeMVar putMVar

-- |Sends a message over the connection.
sendPtr :: Connection Safe -> Ptr Word8 -> Int -> IO ()
sendPtr = sendPtrWith takeMVar putMVar

-- |Blocks till it receives a valid message, placing the resulting plaintext
-- in the provided buffer.  If the incoming message is larger that the
-- provided buffer then the message is truncated.  This process also incurs
-- an additional copy.
recvPtr :: Connection Safe -> Ptr Word8 -> Int -> IO Int
recvPtr = recvPtrWith takeMVar putMVar

-- |Sends a message.
sendUnsafe :: Connection Unsafe -> B.ByteString -> IO ()
sendUnsafe = sendWith readIORef writeIORef

-- |Receives a message.
recvUnsafe :: Connection Unsafe -> IO B.ByteString
recvUnsafe = recvWith readIORef writeIORef

-- |Sends a message.
sendPtrUnsafe :: Connection Unsafe -> Ptr Word8 -> Int -> IO ()
sendPtrUnsafe = sendPtrWith readIORef writeIORef

-- |Blocks till it receives a valid message, placing the resulting plaintext
-- in the provided buffer.  If the incoming message is larger that the
-- provided buffer then the message is truncated.  This process also incurs
-- an additional copy.
recvPtrUnsafe :: Connection Unsafe -> Ptr Word8 -> Int -> IO Int
recvPtrUnsafe = recvPtrWith readIORef writeIORef

-- helper for send, sendUnsafe
sendWith :: (c OutContext -> IO OutContext) -> (c OutContext -> OutContext -> IO ()) -> Connection c -> B.ByteString -> IO ()
sendWith get put conn msg = B.useAsCStringLen msg $ \(ptPtr, ptLen) ->
  sendPtrWith get put conn (castPtr ptPtr) ptLen
{-# INLINE sendWith #-}

data RecvRes = Good | Small | Err deriving (Eq)

-- helper for recv, recvUnsafe
recvWith :: (c InContext -> IO InContext) -> (c InContext -> InContext -> IO ()) -> Connection c -> IO B.ByteString
recvWith get put conn@(Conn {..}) = allocGo baseSize
 where
    baseSize = 1024
    allocGo :: Int -> IO B.ByteString
    allocGo n = allocaBytes sizeTagLen (go n)

    --

    go :: Int -> Ptr Word8 -> IO B.ByteString
    go sz tmpPtr
      | sz > 2^28 = error "recvWith: A message is over 256MB! Probably corrupt data or the stream is unsyncronized."
      | otherwise = do
          recvBytesPtr socket tmpPtr sizeTagLen
          sz <- fromIntegral `fmap` peekBE32 tmpPtr
          (b,res) <- B.createAndTrim' sz $ \ptPtr -> do
            resSz <- recvPtrOfSz get put conn ptPtr sz
            case resSz of
                Left err -> if err `elem` retryOn then return (0,0,Err)
                                                  else throw err
                Right s  ->
                    if s > sz
                        then return (0,0,Small)
                        else return (0,s,Good)
          case res of
              Good   -> return b
              Small  -> go (sz * 2) tmpPtr
              Err    -> go sz tmpPtr
{-# INLINE recvWith #-}

retryOn :: [CommSecError]
retryOn = [DuplicateSeq, InvalidICV, BadPadding]

-- helper for sendPtr, sendPtrUnsafe
sendPtrWith :: (c OutContext -> IO OutContext) -> (c OutContext -> OutContext -> IO ()) -> Connection c -> Ptr Word8 -> Int -> IO ()
sendPtrWith get put c@(Conn {..}) ptPtr ptLen = do
    let ctLen  = encBytes ptLen
        pktLen = sizeTagLen + ctLen
    allocaBytes pktLen $ \pktPtr -> do
        let ctPtr = pktPtr `plusPtr` sizeTagLen
        pokeBE32 pktPtr (fromIntegral ctLen)
        o  <- get outCtx
        o2 <- encodePtr o ptPtr ctPtr ptLen
        put outCtx o2
        Net.sendBuf socket pktPtr pktLen
        return ()

-- helper for recvPtr, recvPtrUnsafe
recvPtrWith :: (c InContext -> IO InContext) -> (c InContext -> InContext -> IO ()) -> Connection c -> Ptr Word8 -> Int -> IO Int
recvPtrWith get put c@(Conn{..}) ptPtr maxLen = do
    r <- go
    case r of
        Nothing  -> recvPtrWith get put c ptPtr maxLen
        Just res -> return res
 where
  go :: IO (Maybe Int)
  go = allocaBytes sizeTagLen $ \szPtr -> do
    recvBytesPtr socket szPtr sizeTagLen
    len <- fromIntegral `fmap` peekBE32 szPtr
    let ptMaxSize = decBytes (len - sizeTagLen)
    allocaBytes len $ \ctPtr -> do
      recvBytesPtr socket ctPtr len
      i <- get inCtx
      let finish pointer = do
              dRes <- decodePtr i ctPtr pointer len
              case dRes of
                Left err -> if err `elem` retryOn then return Nothing
                                                  else throw err
                Right (resLen,i2) -> put inCtx i2 >> return (Just resLen)
      if ptMaxSize > maxLen
          then allocaBytes ptMaxSize (\tmp -> do
                        res <- finish tmp
                        B.memcpy ptPtr tmp maxLen
                        return res)
          else finish ptPtr

-- Receive sz bytes and decode it into ptPtr, helper for recvWith
recvPtrOfSz :: (c InContext -> IO InContext) -> (c InContext -> InContext -> IO ()) -> Connection c -> Ptr Word8 -> Int -> IO (Either CommSecError Int)
recvPtrOfSz get put (Conn {..}) ptPtr sz =
    allocaBytes sz $ \ct -> do
        recvBytesPtr socket ct sz
        i <- get inCtx
        dRes <- decodePtr i ct ptPtr sz
        case dRes of
                Left err -> return (Left err)
                Right (resLen,i2) -> put inCtx i2 >> return (Right resLen)

-- Retry until we have received exactly the specified number of bytes
recvBytesPtr :: Socket -> Ptr Word8 -> Int -> IO ()
recvBytesPtr s p 0 = return ()
recvBytesPtr s p l = do
        nr <- recvBuf s l p
        recvBytesPtr s (p `plusPtr` nr) (l - nr)

-- Retry until we have sent exactly the specified number of bytes
sendBytesPtr :: Socket -> Ptr Word8 -> Int -> IO ()
sendBytesPtr s p 0 = return ()
sendBytesPtr s p l = do
        nr <- sendBuf s p l
        sendBytesPtr s (p `plusPtr` nr) (l - nr)

-- Use counter mode to expand input entropy that is at least 16 bytes long
expandSecret :: B.ByteString -> Int -> B.ByteString
expandSecret entropy sz =
    let k = buildKey entropy
    in case k of
           Nothing  -> error "Build key failed"
           Just key ->
              let iv = B.replicate 16 0
              in enc key iv input
 where
  input = B.replicate sz 0
  enc :: AESKey -> B.ByteString -> B.ByteString -> B.ByteString
  enc k i pt = B.unsafeCreate sz $ \ctPtr ->
                B.useAsCString pt $ \ptPtr ->
                 B.useAsCString i $ \iv ->
                   encryptCTR k (castPtr iv) (castPtr ctPtr) (castPtr ptPtr) sz

-- |Expands the provided 128 (or more) bit secret into two
-- keys to create a connection.
--
-- ex: accept ent me.com 3134 Stream
accept  :: B.ByteString -> PortNumber -> SocketType -> IO (Connection Safe)
accept = doAccept newMVar

doAccept  :: (forall x. x -> IO (c x)) -> B.ByteString -> PortNumber -> SocketType -> IO (Connection c)
doAccept create s p streamOrDgram
  | B.length s < 16 = error "Invalid input entropy"
  | otherwise = do
    let ent   = expandSecret s 64
        k1    = B.take 32 ent
        k2    = B.drop 32 ent
        iCtx  = newInContext k1
        oCtx  = newOutContext k2
        sockaddr = Net.SockAddrInet p Net.iNADDR_ANY
    sock <- Net.socket Net.AF_INET streamOrDgram Net.defaultProtocol
    Net.setSocketOption sock Net.ReuseAddr 1
    Net.bind sock sockaddr
    Net.listen sock 10
    socket <- fst `fmap` Net.accept sock
    Net.setSocketOption socket Net.NoDelay 1
    Net.close sock
    inCtx  <- create iCtx
    outCtx <- create oCtx
    return (Conn {..})

doConnect  :: (forall x. x -> IO (c x)) -> B.ByteString -> HostName -> PortNumber -> SocketType -> IO (Connection c)
doConnect create s hn p streamOrDgram
  | B.length s < 16 = error "Invalid input entropy"
  | otherwise = do
    ha <- Net.inet_addr hn
    let ent  = expandSecret s 64
        k2   = B.take 32 ent
        k1   = B.drop 32 ent
        iCtx = newInContext k1
        oCtx = newOutContext k2
        sockaddr = Net.SockAddrInet p ha
    socket <- Net.socket Net.AF_INET streamOrDgram Net.defaultProtocol
    Net.connect socket sockaddr
    Net.setSocketOption socket Net.NoDelay 1
    Net.setSocketOption socket Net.ReuseAddr 1
    inCtx  <- create iCtx
    outCtx <- create oCtx
    return (Conn {..})

-- |Expands the provided 128 (or more) bit secret into two
-- keys to create a connection.
connect :: B.ByteString
        -> HostName
        -> PortNumber
        -> SocketType
        -> IO (Connection Safe)
connect = doConnect newMVar

-- |Expands the provided 128 (or more) bit secret into two
-- keys to create a connection.
acceptUnsafe :: B.ByteString
             -> PortNumber
             -> SocketType
             -> IO (Connection Unsafe)
acceptUnsafe = doAccept newIORef

-- |Expands the provided 128 (or more) bit secret into two
-- keys to create a connection.
connectUnsafe :: B.ByteString
              -> HostName
              -> PortNumber
              -> SocketType
              -> IO (Connection Unsafe)
connectUnsafe = doConnect newIORef

-- |We use a word32 to indicate the size of a datagram
sizeTagLen = 4