diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6491ed2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.sw[pon] +*.hi +*.o +.stack-work +Example diff --git a/Example.hs b/Example.hs new file mode 100644 index 0000000..3c7a6f3 --- /dev/null +++ b/Example.hs @@ -0,0 +1,163 @@ +import Prelude +import System.Environment (getArgs) +import Network.Socket hiding (recv) +import Network.Socket.ByteString (recv, sendAll) +import Control.Concurrent +import Control.Concurrent.MVar +import Data.Monoid ((<>)) +import Data.Serialize +import Control.Monad.State +import Control.Applicative (liftA) +import Crypto.Error +import Crypto.PubKey.Curve25519 +import Crypto.Random +import qualified Data.ByteArray as BA +import qualified Data.ByteArray.Encoding as BAE +import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as BSC + +import Ratchet + +data Input = + Keyboard BS.ByteString + | Wire BS.ByteString + +port :: String +port = "12345" + +host :: String +host = "localhost" + +main :: IO () +main = do + let port = "12345" + host = "localhost" + arg <- getArgs + + case arg of + ["server"] -> server + ["client"] -> client + _ -> usage + +maxSkipMsgs :: MaxSkipped +maxSkipMsgs = 64 + +client :: IO () +client = withSocketsDo $ do + let stateBob = emptyRatchet maxSkipMsgs + (ssk, bobsKeyPair) <- readSharedData + mvar <- newEmptyMVar + addrinfos <- getAddrInfo Nothing (Just host) (Just port) + let serveraddr = head addrinfos + conn <- socket (addrFamily serveraddr) Stream defaultProtocol + connect conn (addrAddress serveraddr) + putStrLn "Connected to server." + forkFinally (wireLoop conn mvar) (\_ -> close conn) + forkIO $ inpLoop conn mvar + evalState' stateBob $ do + ratchetInitBob ssk bobsKeyPair + handle conn mvar + +server :: IO () +server = withSocketsDo $ do + let maxConnQueue = 1 + stateAlice = emptyRatchet maxSkipMsgs + (ssk, bobsKeyPair) <- readSharedData + + mvar <- newEmptyMVar + addrinfos <- getAddrInfo + (Just $ defaultHints {addrFlags = [AI_PASSIVE]}) + Nothing + (Just port) + let serveraddr = head addrinfos + sock <- socket (addrFamily serveraddr) Stream defaultProtocol + setSocketOption sock ReuseAddr 1 + bind sock (addrAddress serveraddr) + listen sock maxConnQueue + + putStrLn "Waiting for client to connect..." + (conn, _) <- accept sock + putStrLn "Client connected." + forkFinally (wireLoop conn mvar) (\_ -> close conn) + forkIO $ inpLoop conn mvar + evalState' stateAlice $ do + ratchetInitAlice ssk (pubKey bobsKeyPair) + handle conn mvar + +handle :: Socket -> MVar Input -> StateT StateRatchet IO () +handle conn mvar = do + inp <- lift $ takeMVar mvar + + case inp of + Keyboard str -> do + let adLen = 42 + ad <- lift $ AssocData <$> getRandomBytes adLen + (mh, payload) <- ratchetEncrypt (PlainText $ BA.convert str) ad + case payload of + Left err -> + lift $ print err + + Right cipher -> do + let msg = encode (mh, cipher, ad) + lift $ sendAll conn msg + + Wire msg -> do + case decode msg of + Left err -> + lift $ putStrLn $ "Could not decode received data: " <> err + + Right (mh, cipher, ad) -> do + dec <- ratchetDecrypt mh cipher ad + case dec of + Left err -> + lift $ print err + + Right (PlainText plain) -> do + let pre = "\nMessage received: " + lift $ putStrLn $ pre <> show plain + + handle conn mvar + +inpLoop :: Socket -> MVar Input -> IO () +inpLoop conn mvar = do + putStr "Type a message: " + str <- getLine + putMVar mvar $ Keyboard $ BSC.pack str + + inpLoop conn mvar + +wireLoop :: Socket -> MVar Input -> IO () +wireLoop conn mvar = do + bs <- recv conn 1024 + putMVar mvar $ Wire bs + + wireLoop conn mvar + +usage :: IO () +usage = putStrLn "stack repl Example.hs \"server\" | \"client\"" + +-- helper functions +evalState' :: Monad m => s -> StateT s m a -> m a +evalState' = flip evalStateT + +readSharedData :: IO (SharedSecretKey, DHKeyPair) +readSharedData = do + let extract pub priv = do + priv' <- secretKey priv + pub' <- publicKey pub + return (priv', pub') + let convert = BAE.convertFromBase BAE.Base64 + + Right bytes <- liftA convert $ BS.readFile "ssk" + let ssk = SharedSecretKey bytes + + Right pub <- liftA convert $ BS.readFile "bob_public" + Right priv <- liftA convert $ BS.readFile "bob_private" + + let keys = extract pub priv + case keys of + CryptoFailed err -> + undefined + + CryptoPassed (priv', pub') -> + return (ssk, DHKeyPair priv' pub') diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c5f08ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017 Adjoint Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6d87ce --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +Double Ratchet Algorithm +======================== + +An implementation of Open Whisper System's Double Ratchet Algorithm in Haskell. + +The Double Ratchet algorithm is used by two parties to exchange encrypted +messages based on a shared secret key. Typically the parties will use some key +agreement protocol (such as X3DH) to agree on the shared secret key. Following +this, the parties will use the Double Ratchet to send and receive encrypted +messages. + +The parties derive new keys for every Double Ratchet message so that earlier +keys cannot be calculated from later ones. The parties also send Diffie-Hellman +public values attached to their messages. The results of Diffie-Hellman +calculations are mixed into the derived keys so that later keys cannot be +calculated from earlier ones. These properties gives some protection to earlier +or later encrypted messages in case of a compromise of a party's keys. + +Usage +----- + +The example will use the pre-shared keys `bob_private` and `bob_public` and `ssk`. + +To compile the example: + +```bash +> stack ghc Example.hs --package ratchet +> ./Example server +> ./Example client +``` + +To load in GHCi: + +```bash +> stack ghci --package network +> :load Example.hs +``` + +Protocol +-------- + +After an initial key exchange it manages the ongoing renewal and maintenance of +short-lived session keys. It combines a cryptographic ratchet based on the +Diffie–Hellman key exchange (DH) and a ratchet based on a key derivation +function (KDF) like e.g. a hash function and is therefore called a double +ratchet. + +* [The Double Ratchet Algorithm](https://whispersystems.org/docs/specifications/doubleratchet/) + +Signal Protocol protocol combines the Double Ratchet Algorithm, prekeys, and a +triple Diffie–Hellman (3-DH) handshake, and uses Curve25519, AES-256 and +HMAC-SHA256 as primitives. + +* [Extended Triple Diffie-Hellman](https://whispersystems.org/docs/specifications/x3dh/) + +License +------- + +``` +Copyright 2017 Adjoint Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/bob_private b/bob_private new file mode 100644 index 0000000..7be1df1 --- /dev/null +++ b/bob_private @@ -0,0 +1 @@ +oGUnGXXfxtL/rhnm35wipxnscw6sXJGRv+ro/1c941Q= \ No newline at end of file diff --git a/bob_public b/bob_public new file mode 100644 index 0000000..72a3fc8 --- /dev/null +++ b/bob_public @@ -0,0 +1 @@ +uXKpMtSLxC073z3OwKue+riryfTPX1Z1SmybLTn6fHg= \ No newline at end of file diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..cbe3780 --- /dev/null +++ b/circle.yml @@ -0,0 +1,17 @@ +dependencies: + cache_directories: + - "~/.stack" + - "/home/ubuntu/ratchet/.stack-work" + pre: + - wget https://github.com/commercialhaskell/stack/releases/download/v1.4.0/stack-1.4.0-linux-x86_64.tar.gz -O /tmp/stack.tar.gz + - tar -zxvf /tmp/stack.tar.gz -C /tmp + - sudo mv /tmp/stack-**/stack /usr/bin/stack + - sudo apt-get update -q + + override: + - stack setup --resolver lts-8.5 --no-docker + - stack build --fast --no-docker + +test: + override: + - stack test --no-docker diff --git a/ratchet.cabal b/ratchet.cabal new file mode 100644 index 0000000..881a807 --- /dev/null +++ b/ratchet.cabal @@ -0,0 +1,128 @@ +name: ratchet +version: 0.1 +synopsis: Double Ratchet Algorithm +license: Apache +license-file: LICENSE +author: Adjoint Inc. +maintainer: info@adjoint.io +copyright: 2017 Adjoint Inc +Category: Cryptography +build-type: Simple +cabal-version: >=1.10 + +description: + An implementation of Open Whisper System's Double Ratchet Algorithm in Haskell + +Source-Repository head + Type: git + Location: git@github.com: adjoint-io/double-ratchet + +library + exposed-modules: + Ratchet + + other-modules: + Ratchet.Types, Ratchet.Defaults + + build-depends: + base >= 4.6 && <5.0, + transformers >= 0.4 && <0.6, + unordered-containers >= 0.2 && <0.3, + protolude >= 0.1.10 && <0.2, + memory >= 0.14 && <0.15, + text >= 1.2 && <1.3, + cereal >= 0.5 && <0.6, + bytestring >= 0.10 && <0.11, + cryptonite >= 0.20 && <0.30, + mtl >= 2.2 && <2.3, + either >= 4.4 && <4.5 + + default-language: Haskell2010 + + default-extensions: + LambdaCase + RecordWildCards + OverloadedStrings + NoImplicitPrelude + FlexibleInstances + + hs-source-dirs: src + ghc-options: + -fwarn-tabs + -fwarn-incomplete-patterns + -fwarn-incomplete-record-updates + -fwarn-redundant-constraints + -fwarn-implicit-prelude + -fwarn-overflowed-literals + -fwarn-orphans + -fwarn-identities + -fwarn-dodgy-exports + -fwarn-dodgy-imports + -fwarn-duplicate-exports + -fwarn-overlapping-patterns + -fwarn-missing-fields + -fwarn-missing-methods + -fwarn-missing-signatures + -fwarn-noncanonical-monad-instances + -fwarn-unused-pattern-binds + -fwarn-unused-type-patterns + -fwarn-unrecognised-pragmas + -fwarn-wrong-do-bind + -fno-warn-name-shadowing + -fno-warn-unused-binds + -fno-warn-unused-matches + -fno-warn-unused-do-bind + +test-suite ratchet-test + type: exitcode-stdio-1.0 + main-is: Main.hs + hs-source-dirs: + test + build-depends: + base >= 4.7 && < 5 + , ratchet + , protolude + , unordered-containers + , text + , bytestring + , mtl + , cryptonite + , memory + , protolude + , QuickCheck + , tasty + , tasty-quickcheck + , quickcheck-instances + default-language: Haskell2010 + default-extensions: + LambdaCase + RecordWildCards + OverloadedStrings + NoImplicitPrelude + FlexibleInstances + + ghc-options: + -fwarn-tabs + -fwarn-incomplete-patterns + -fwarn-incomplete-record-updates + -fwarn-redundant-constraints + -fwarn-implicit-prelude + -fwarn-overflowed-literals + -fwarn-orphans + -fwarn-identities + -fwarn-dodgy-exports + -fwarn-dodgy-imports + -fwarn-duplicate-exports + -fwarn-overlapping-patterns + -fwarn-missing-fields + -fwarn-missing-methods + -fwarn-missing-signatures + -fwarn-noncanonical-monad-instances + -fwarn-unused-pattern-binds + -fwarn-unused-type-patterns + -fwarn-unrecognised-pragmas + -fwarn-wrong-do-bind + -fno-warn-name-shadowing + -fno-warn-unused-binds + -fno-warn-unused-matches + -fno-warn-unused-do-bind diff --git a/src/Ratchet.hs b/src/Ratchet.hs new file mode 100644 index 0000000..e01fac0 --- /dev/null +++ b/src/Ratchet.hs @@ -0,0 +1,213 @@ +{-| + +An implementation of Open Whisper System's Double Ratchet Algorithm in Haskell. + +The Double Ratchet algorithm is used by two parties to exchange encrypted +messages based on a shared secret key. Typically the parties will use some key +agreement protocol (such as X3DH) to agree on the shared secret key. Following +this, the parties will use the Double Ratchet to send and receive encrypted +messages. + +The parties derive new keys for every Double Ratchet message so that earlier +keys cannot be calculated from later ones. The parties also send Diffie-Hellman +public values attached to their messages. The results of Diffie-Hellman +calculations are mixed into the derived keys so that later keys cannot be +calculated from earlier ones. These properties gives some protection to earlier +or later encrypted messages in case of a compromise of a party's keys. + +-} + +module Ratchet ( + module Ratchet.Types + , ratchetInitAlice + , ratchetInitBob + , ratchetEncrypt + , ratchetDecrypt + , genSharedSecret + , generateDH +) where + +import Protolude hiding (concat) + +import qualified Data.HashMap.Strict as HMap +import qualified Crypto.PubKey.Curve25519 as C25519 +import qualified Data.ByteArray as B +import Data.Maybe (fromJust) +import Data.Serialize (decode) +import Crypto.Random (MonadRandom, getRandomBytes) + +import Ratchet.Types +import Ratchet.Defaults + +-- | Initialises ratchet state for Alice (the party sending the first message) +ratchetInitAlice :: MonadRandom m => SharedSecretKey + -> C25519.PublicKey + -> StateT StateRatchet m () +ratchetInitAlice sk bobDHPublicKey = do + state <- get + dhs <- lift generateDH + let dhr = B.convert bobDHPublicKey + let dhOut = dh dhs dhr + (rk', cks) = kdfRk (sskToRk sk) dhOut + state' = + state { dh_s = dhs + , dh_r = Just dhr + , rk = rk' + , ck_s = cks + , ck_r = Nothing + , n_s = 0 + , n_r = 0 + , p_n = 0 + , mk_skipped = HMap.empty + } + put state' + +-- | Initialises ratchet state for Bob (the party receiving the first message) +ratchetInitBob :: MonadRandom m => SharedSecretKey + -> DHKeyPair + -> StateT StateRatchet m () +ratchetInitBob sk bobDHKeyPair = do + state <- get + let state' = + state { dh_s = bobDHKeyPair + , dh_r = Nothing + , rk = sskToRk sk + , ck_r = Nothing + , n_s = 0 + , n_r = 0 + , p_n = 0 + , mk_skipped = HMap.empty + } + put state' + +ratchetEncrypt :: MonadRandom m => PlainText + -> AssocData + -> StateT StateRatchet m (MessageHeader, Either RatchetError AEADCipherText) -- BEAUTIFY ME +ratchetEncrypt plain ad = do + state <- get + let (cks, mk) = kdfCk $ ck_s state + headerVal = header (dh_s state) (p_n state) (n_s state) + n_s' = n_s state + 1 + state' = state { ck_s = cks + , n_s = n_s' + } + enc = encrypt mk plain $ concat ad headerVal + put state' + return (headerVal, enc) + +ratchetDecrypt :: MonadRandom m => MessageHeader + -> AEADCipherText + -> AssocData + -> StateT StateRatchet m (Either RatchetError PlainText) +ratchetDecrypt mh aeadCipher ad = do + val <- trySkippedMessageKeys mh aeadCipher ad + case val of + Just plain -> + return $ Right plain + + Nothing -> do + state <- get + let dhr = dh_r state + if dhr == Nothing || + dh_ratchet_pub_key mh /= (B.convert $ fromJust dhr) + then do + skipMessageKeys $ pn_mh mh + dhRatchet mh + finishIt + else finishIt + + where + finishIt = do + skipMessageKeys $ msg_number mh + state <- get + let (ckr, mk) = kdfCk $ fromJust $ ck_r state + let state' = state { ck_r = Just ckr + , n_r = n_r state + 1 + } + put state' + return $ decrypt mk aeadCipher $ concat ad mh + +trySkippedMessageKeys :: MonadRandom m => MessageHeader + -> AEADCipherText + -> AssocData + -> StateT StateRatchet m (Maybe PlainText) +trySkippedMessageKeys mh aeadCipher ad = do + state <- get + let msgMap = mk_skipped state + key = (dh_ratchet_pub_key mh, msg_number mh) + case HMap.lookup key msgMap of + Nothing -> + return Nothing + + Just mk -> do + let msgMap' = HMap.delete key msgMap + state' = state { mk_skipped = msgMap' } + put state' + return $ rightToMaybe $ decrypt mk aeadCipher $ concat ad mh + +skipMessageKeys :: MonadRandom m => Int -> StateT StateRatchet m (Either RatchetError ()) +skipMessageKeys until = do + state <- get + if n_r state + (max_skip state) < until + then return $ Left SkipMessageKeys + else case ck_r state of + Nothing -> return $ Right () + + Just _ -> insert $ n_r state + + where + insert :: MonadRandom m => Int -> StateT StateRatchet m (Either RatchetError ()) + insert ctr + | ctr >= until = return $ Right () + | otherwise = do + state <- get + let ckr = fromJust $ ck_r state + dhr = dh_r state + nr = ctr + msgMap = mk_skipped state + (ckr', mk) = kdfCk ckr + msgMap' = HMap.insert (fromJust dhr, nr) mk msgMap + nr' = nr + 1 + state' = state { mk_skipped = msgMap' + , n_r = nr' + , ck_r = Just ckr' + } + put state' + insert $ ctr + 1 + +dhRatchet :: MonadRandom m => MessageHeader + -> StateT StateRatchet m () +dhRatchet mh = do + state <- get + let pn = n_s state + ns = 0 + nr = 0 + dhr = dh_ratchet_pub_key mh + (rk', ckr) = kdfRk (rk state) $ + dh (dh_s state) dhr + dhs <- lift generateDH + let (rk'', cks) = kdfRk rk' $ dh dhs $ B.convert dhr + state' = state { dh_s = dhs + , rk = rk'' + , dh_r = Just dhr + , n_s = ns + , n_r = nr + , p_n = pn + , ck_r = Just ckr + , ck_s = cks + } + put state' + +-- | Returns a new shared secret to be used when initialising the states. +genSharedSecret :: MonadRandom m => m SharedSecretKey +genSharedSecret = SharedSecretKey <$> getRandomBytes keyLen + +-- | Returns a new Diffie-Hellman key pair. +generateDH :: MonadRandom m => m DHKeyPair +generateDH = do + sk <- C25519.generateSecretKey + let pk = C25519.toPublic sk + return $ DHKeyPair sk pk + +sskToRk :: SharedSecretKey -> RootKey +sskToRk (SharedSecretKey k) = RootKey k diff --git a/src/Ratchet/Defaults.hs b/src/Ratchet/Defaults.hs new file mode 100644 index 0000000..aafccfc --- /dev/null +++ b/src/Ratchet/Defaults.hs @@ -0,0 +1,175 @@ +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +module Ratchet.Defaults where + +import Protolude hiding ((<>)) +import Data.Monoid ((<>)) + +import qualified Crypto.PubKey.Curve25519 as C25519 +import qualified Crypto.KDF.HKDF as HKDF +import qualified Data.ByteArray as B +import qualified Crypto.MAC.HMAC as HMAC +import qualified Data.Text as T +import Data.Serialize +import Crypto.Error +import Crypto.Cipher.AES +import Crypto.Random.Types +import Crypto.Hash +import Crypto.Cipher.Types +import Crypto.Data.Padding (unpad, pad, Format(..)) + +import Ratchet.Types + +-- | Returns the output from the Diffie-Hellman calculation between the private +-- key from the DH key pair dh_pair and the DH public key dh_pub. If the DH +-- function rejects invalid public keys, then this function may raise an +-- exception which terminates processing. +-- +-- > DH(dh_pair, dh_pub): +dh :: DHKeyPair -> ByteString -> C25519.DhSecret +dh pair pubk = + case C25519.publicKey pubk of + CryptoFailed err -> + panic $ T.pack $ show err + + CryptoPassed pubk' -> + C25519.dh pubk' $ privKey pair + +-- This function is recommended to be implemented using HKDF [3] with SHA-256 or +-- SHA-512 [8], using rk as HKDF salt, dh_out as HKDF input key material, and an +-- application-specific byte sequence as HKDF info. The info value should be +-- chosen to be distinct from other uses of HKDF in the application. Returns a +-- pair (32-byte root key, 32-byte chain key) as the output of applying a KDF +-- keyed by a 32-byte root key rk to a Diffie-Hellman output dh_out. +-- +-- > KDF_RK(rk, dh_out): +kdfRk :: RootKey -> C25519.DhSecret -> (RootKey, ChainKey) +kdfRk (RootKey rk) dhOut = + let prk = HKDF.extract rk dhOut :: HKDF.PRK SHA512 + keys = HKDF.expand prk info keyLens + (rk', ck) = B.splitAt keyLen keys + in (RootKey rk', ChainKey ck) + + where + keyLens = keyLen * 2 + info = "\xDE\xAD\xCO\xDE" :: B.ScrubbedBytes + +-- | Returns a pair (32-byte chain key, 32-byte message key) as the output of +-- applying a KDF keyed by a 32-byte chain key ck to some constant. +-- +-- HMAC [2] with SHA-256 or SHA-512 [8] is recommended, using ck as the HMAC key +-- and using separate constants as input (e.g. a single byte 0x01 as input to +-- produce the message key, and a single byte 0x02 as input to produce the next +-- chain key). +kdfCk :: ChainKey -> (ChainKey, MessageKey) +kdfCk (ChainKey ck) = + let ck' = HMAC.hmac ck ckMsg :: HMAC.HMAC SHA512 + mk = HMAC.hmac ck mkMsg :: HMAC.HMAC SHA512 + in (ChainKey $ B.convert ck', MessageKey $ B.convert mk) + + where + ckMsg = "\x02" :: B.ScrubbedBytes + mkMsg = "\x01" :: B.ScrubbedBytes + +-- | Returns an AEAD encryption of plaintext with message key mk [5]. The +-- associated_data is authenticated but is not included in the ciphertext. +-- Because each message key is only used once, the AEAD nonce may handled in +-- several ways: fixed to a constant; derived from mk alongside an independent +-- AEAD encryption key; derived as an additional output from KDF_CK(); or chosen +-- randomly and transmitted. +encrypt :: MessageKey -> PlainText -> AssocData + -> Either RatchetError AEADCipherText +encrypt mk plainText (AssocData assocData) = do + let (encKey, authKey, iv) = genKeys mk + (CipherText cipherText) <- encryptAES256 plainText encKey iv + let authMsg = assocData <> cipherText + hmacVal = HMAC.hmac authKey authMsg :: HMAC.HMAC SHA512 + return $ AEADCipherText $ cipherText <> (B.convert hmacVal) + + where + encryptAES256 :: PlainText -> B.ScrubbedBytes + -> Maybe (IV AES256) + -> Either RatchetError CipherText + encryptAES256 _ _ Nothing = Left $ InternalError CryptoError_IvSizeInvalid + encryptAES256 (PlainText plain) encKey (Just iv) = + case cipherInit encKey :: CryptoFailable AES256 of + CryptoFailed err -> + Left $ InternalError err + + CryptoPassed c -> do + let padded = pad (PKCS7 $ blockSize c) plain + return $ CipherText $ cbcEncrypt c iv padded + +genKeys :: MessageKey + -> (B.ScrubbedBytes, B.ScrubbedBytes, Maybe (IV AES256)) +genKeys (MessageKey mk) = + let prk = HKDF.extract hkdfSalt mk :: HKDF.PRK SHA512 + keys = HKDF.expand prk info outLength + (encKey, authKey, iv) = extractKeys keys + iv' = makeIV iv :: Maybe (IV AES256) + in (encKey, authKey, iv') + + where + hkdfSalt = B.replicate outLength (0 :: Word8) :: B.Bytes + outLength = encKeyLen + authKeyLen + ivLen + info = "\x01\x03\x03\x07" :: ByteString + encKeyLen = keyLen -- 32 + authKeyLen = keyLen -- 32 + ivLen = 16 + + extractKeys keys = + let (encKey, rest) = B.splitAt encKeyLen keys + (authKey, iv) = B.splitAt authKeyLen rest + in (encKey, authKey, iv) + +-- | Returns the AEAD decryption of ciphertext with message key mk. If +-- authentication fails, an exception will be raised that terminates processing. +decrypt :: MessageKey -> AEADCipherText -> AssocData + -> Either RatchetError PlainText +decrypt mk (AEADCipherText aeadCipherText) (AssocData assocData) = do + let cutOff = B.length aeadCipherText - hashDigestSize SHA512 + (cipherText, digest) = B.splitAt cutOff aeadCipherText + (decKey, authKey, iv) = genKeys mk + pt@(PlainText plainText) <- decryptAES256 (CipherText cipherText) decKey iv + let authMsg = assocData <> cipherText + hmacVal = B.convert (HMAC.hmac authKey authMsg :: HMAC.HMAC SHA512) + if hmacVal /= digest + then Left HMACMismatch + else return pt + + where + decryptAES256 :: CipherText -> B.ScrubbedBytes + -> Maybe (IV AES256) + -> Either RatchetError PlainText + decryptAES256 _ _ Nothing = + Left $ InternalError $ CryptoError_IvSizeInvalid + decryptAES256 (CipherText cipherText) decKey (Just iv) = + case cipherInit decKey :: CryptoFailable AES256 of + CryptoFailed err -> + Left $ InternalError err + + CryptoPassed c -> do + let padded = cbcDecrypt c iv cipherText + case unpad (PKCS7 $ blockSize c) padded of + Nothing -> + Left UnpaddingFailure + + Just plain -> + return $ PlainText plain + +-- | Creates a new message header containing the DH ratchet public key from the +-- key pair in dh_pair, the previous chain length pn, and the message number n. +-- The returned header object contains ratchet public key dh and integers pn and +-- n. +type PrevChainLen = Int +header :: DHKeyPair -> PrevChainLen -> MsgNum -> MessageHeader +header pair pn_ n = + MessageHeader (B.convert $ pubKey pair) pn_ n + +-- | t Encodes a message header into a parseable byte sequence, prepends the ad +-- byte sequence, and returns the result. If ad is not guaranteed to be a +-- parseable byte sequence, a length value should be prepended to the output to +-- ensure that the output is parseable as a unique pair (ad, header). +concat :: AssocData -> MessageHeader -> AssocData +concat (AssocData ad) h = + AssocData $ ad <> (B.convert . encode) h diff --git a/src/Ratchet/Types.hs b/src/Ratchet/Types.hs new file mode 100644 index 0000000..6683f92 --- /dev/null +++ b/src/Ratchet/Types.hs @@ -0,0 +1,85 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DeriveAnyClass #-} +module Ratchet.Types where + +import Protolude + +import Data.Serialize (Serialize) +import Crypto.Error (CryptoError) +import qualified Data.ByteArray as B +import qualified Data.Text as T +import qualified Data.HashMap.Strict as Map +import qualified Crypto.PubKey.Curve25519 as C25519 + +keyLen :: Int +keyLen = 32 + +data RatchetError = + InternalError CryptoError + | HMACMismatch + | UnpaddingFailure + | SkipMessageKeys + deriving (Show, Eq) + +data MessageHeader = MessageHeader + { dh_ratchet_pub_key :: ByteString -- ^ public key + , pn_mh :: Int -- ^ previous chain length + , msg_number :: Int + } deriving (Show, Generic, Serialize) + +newtype SharedSecretKey = + SharedSecretKey B.ScrubbedBytes -- 32 bytes + +newtype RootKey = + RootKey B.ScrubbedBytes deriving Show -- 32 bytes + +newtype ChainKey = + ChainKey B.ScrubbedBytes deriving Show -- 32 bytes + +newtype MessageKey = + MessageKey B.ScrubbedBytes deriving Show -- 32 bytes + +newtype PlainText = + PlainText ByteString deriving (Show, Eq) + +newtype AEADCipherText = + AEADCipherText ByteString deriving (Show, Generic, Serialize) + +newtype CipherText = + CipherText ByteString deriving Show + +newtype AssocData = + AssocData ByteString deriving (Show, Generic, Serialize) + +data DHKeyPair = DHKeyPair + { privKey :: C25519.SecretKey -- ^ Curve25519 private key + , pubKey :: C25519.PublicKey -- ^ Curve25519 public key + } deriving Show + +type MsgNum = Int +data StateRatchet = StateRatchet { + dh_s :: DHKeyPair -- ^ DH Ratchet key pair (the "sending" or "self" ratchet key) + , dh_r :: Maybe ByteString -- ^ DH Ratchet public key (the "received" or "remote" key) + , rk :: RootKey + , ck_s :: ChainKey -- ^ sending + , ck_r :: Maybe ChainKey -- ^ receiving + , n_s, n_r :: Int -- ^ message number + , p_n :: Int -- ^ number of messages in previous sending chain + , mk_skipped :: Map.HashMap (ByteString, MsgNum) MessageKey + , max_skip :: MaxSkipped -- ^ max number of message keys that can be skipped in a single chain. +} deriving Show + +type MaxSkipped = Int +emptyRatchet :: MaxSkipped -> StateRatchet +emptyRatchet ms = StateRatchet { + dh_r = Nothing + , ck_s = panic "emptyRatchet: ck_s not set" + , ck_r = Nothing + , n_s = panic "emptyRatchet: n_s not set" + , n_r = panic "emptyRatchet: n_r not set" + , p_n = panic "emptyRatchet: p_n not set" + , mk_skipped = panic "emptyRatchet: mk_skipped not set" + , dh_s = panic "emptyRatchet: dh_s not set" + , rk = panic "emptyRatchet: rk not set" + , max_skip = if ms >= 0 then ms else 0 + } diff --git a/ssk b/ssk new file mode 100644 index 0000000..fea5a9c --- /dev/null +++ b/ssk @@ -0,0 +1 @@ +q6CZh+tchBAXSv5aQcR0yO1bxKJDY0+vQCoVtUGpFC8= \ No newline at end of file diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..e17d3ce --- /dev/null +++ b/stack.yaml @@ -0,0 +1,66 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# http://docs.haskellstack.org/en/stable/yaml_configuration/ + +# Resolver to choose a 'specific' stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# resolver: lts-3.5 +# resolver: nightly-2015-09-21 +# resolver: ghc-7.10.2 +# resolver: ghcjs-0.1.0_ghc-7.10.2 +# resolver: +# name: custom-snapshot +# location: "./custom-snapshot.yaml" +resolver: lts-8.5 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# - location: +# git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# extra-dep: true +# subdirs: +# - auto-update +# - wai +# +# A package marked 'extra-dep: true' will only be built if demanded by a +# non-dependency (i.e. a user package), and its test suites and benchmarks +# will not be run. This is useful for tweaking upstream packages. +packages: +- '.' +# Dependency packages to be pulled from upstream that are not in the resolver +# (e.g., acme-missiles-0.3) +extra-deps: [] + +# Override default flag values for local packages and extra-deps +flags: {} + +# Extra package databases containing global packages +extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=1.3" +# +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor diff --git a/test/Main.hs b/test/Main.hs new file mode 100644 index 0000000..6c32bdb --- /dev/null +++ b/test/Main.hs @@ -0,0 +1,197 @@ +module Main where + +import Protolude hiding ((<>)) +import Data.Monoid ((<>)) + +import Test.Tasty +import Test.QuickCheck.Instances +import Test.QuickCheck.Monadic +import qualified Test.Tasty.QuickCheck as QC + + +import Data.ByteArray +import Data.ByteString hiding (reverse, sort, filter) +import Crypto.Random (getRandomBytes) +import Data.Maybe (fromJust) +import qualified Data.ByteString as B + +import Ratchet + +newtype BS32 = BS32 ByteString deriving Show +instance QC.Arbitrary BS32 where + arbitrary = BS32 <$> arbitraryBS 32 + +-- stolen from cryptonite +arbitraryBS :: Int -> QC.Gen ByteString +arbitraryBS n = B.pack `fmap` replicateM n QC.arbitrary + +main :: IO () +main = defaultMain tests + +tests :: TestTree +tests = testGroup "Tests" [properties] + +properties :: TestTree +properties = testGroup "Properties" [qcProps] + +qcProps :: TestTree +qcProps = testGroup "(checked by QuickCheck)" + [ QC.testProperty "ratchet test: simple" $ + (\plain1 plain2 ad -> monadicIO $ do + b <- run $ doubleRatchetTest (plain1 :: ByteString) + (plain2 :: ByteString) + (ad :: ByteString) + assert b) + , QC.testProperty "ratchet test: ping pong" $ + (\plain1 plain2 ad -> monadicIO $ do + b <- run $ doubleRatchetPingPongTest (plain1 :: ByteString) + (plain2 :: ByteString) + (ad :: ByteString) + assert b) + , QC.testProperty "ratchet test: msgs not ordered" $ + monadicIO $ do + b <- run doubleRatchetNoOrderTest + assert b + ] + +data CombinedState = CombinedState { + alice, bob :: StateRatchet +} + +doubleRatchetTest :: ByteString -> ByteString -> ByteString -> IO Bool +doubleRatchetTest plain1 plain2 ad = do + let plain1' = PlainText $ convert plain1 + plain2' = PlainText $ convert plain2 + ad' = AssocData $ convert ad + + cs <- initStates + + (val, cs') <- sendReceiveAliceBob plain1' ad' cs + (val', cs'') <- sendReceiveBobAlice plain2' ad' cs' + + return $ val && val' + +doubleRatchetPingPongTest :: ByteString -> ByteString -> ByteString -> IO Bool +doubleRatchetPingPongTest plain1 plain2 ad = do + let plain1' = PlainText $ convert plain1 + plain2' = PlainText $ convert plain2 + ad' = AssocData $ convert ad + nRounds = 10 + loop ctr cs acc + | ctr >= nRounds = return acc + | otherwise = do + (val, cs') <- sendReceiveAliceBob plain1' ad' cs + (val', cs'') <- sendReceiveBobAlice plain2' ad' cs' + + loop (ctr + 1) cs'' $ val : val' : acc + + cs <- initStates + and <$> loop 0 cs [] + +doubleRatchetNoOrderTest :: IO Bool +doubleRatchetNoOrderTest = do + let nMsgs = 10 + idx = 123 + createMessages :: Int -> + [((MessageHeader, Either RatchetError AEADCipherText), PlainText, AssocData)] -> + StateT StateRatchet IO [(MessageHeader, AEADCipherText, PlainText, AssocData)] + createMessages ctr acc + | ctr >= nMsgs = do + let msgs' = filter (\((_, msg), _, _) -> isRight msg) acc + msgs'' = fmap (\((mh, Right msg), p, ad) -> (mh, msg, p, ad)) msgs' + return msgs'' + + | otherwise = do + plain <- lift $ PlainText <$> getRandomBytes 1337 + ad <- lift $ AssocData <$> getRandomBytes 42 + msg <- createMessage plain ad + + createMessages (ctr + 1) $ (msg, plain, ad) : acc + + receiveMessages [] acc = return acc + receiveMessages ((mh, cipher, p, ad):ms) acc = do + msg <- receiveMessage mh cipher ad + let val = msg == Right p + + receiveMessages ms $ val : acc + + cs <- initStates + let stateAlice = alice cs + stateBob = bob cs + (msgs, _) <- runState' stateAlice $ createMessages 0 [] + + let msgs' = fromJust . (flip atMay) idx $ permutations msgs + + (vals, _) <- runState' stateBob $ receiveMessages msgs' [] + + return $ and vals + +initStates :: IO CombinedState +initStates = do + let maxSkipMsgs = 64 + stateAlice = emptyRatchet maxSkipMsgs + stateBob = emptyRatchet maxSkipMsgs + sharedSecret <- genSharedSecret + bobKP <- generateDH + + (_, stateAlice') <- runState' stateAlice $ + ratchetInitAlice sharedSecret (pubKey bobKP) + (_, stateBob') <- runState' stateBob $ + ratchetInitBob sharedSecret bobKP + + return $ CombinedState stateAlice' stateBob' + +sendReceiveAliceBob :: PlainText -> AssocData -> CombinedState -> IO (Bool, CombinedState) +sendReceiveAliceBob = sendReceive + +sendReceiveBobAlice :: PlainText -> AssocData -> CombinedState -> IO (Bool, CombinedState) +sendReceiveBobAlice plainT ad cs = do + let cs' = swapStates cs + (val, cs'') <- sendReceive plainT ad cs' + let cs''' = swapStates cs'' + return (val, cs''') + +sendReceive :: PlainText + -> AssocData + -> CombinedState + -> IO (Bool, CombinedState) +sendReceive plainT@(PlainText pt) ad cs = do + let (CombinedState stateSender stateReceiver) = cs + ((mh, cipher), stateSender') <- runState' stateSender $ + ratchetEncrypt plainT ad + + case cipher of + Left err -> do + print err + let cs' = CombinedState stateSender' stateReceiver + return (False, cs') + + Right cipherText -> do + (plain, stateReceiver') <- runState' stateReceiver $ do + ratchetDecrypt mh cipherText ad + let cs' = CombinedState stateSender' stateReceiver' + case plain of + Left err -> do + print err + return (False, cs') + + Right (PlainText plainText) -> + return (plainText `constEq` pt, cs') + +receiveMessage :: MessageHeader + -> AEADCipherText + -> AssocData + -> StateT StateRatchet IO (Either RatchetError PlainText) +receiveMessage = ratchetDecrypt + +createMessage :: PlainText + -> AssocData + -> StateT + StateRatchet IO (MessageHeader, Either RatchetError AEADCipherText) +createMessage = ratchetEncrypt + +swapStates :: CombinedState -> CombinedState +swapStates (CombinedState a b) = CombinedState b a + +runState' :: s -> StateT s m a -> m (a, s) +runState' = flip runStateT