diff options
-rw-r--r-- | Tox.hs | 263 |
1 files changed, 263 insertions, 0 deletions
@@ -0,0 +1,263 @@ | |||
1 | {-# LANGUAGE DeriveDataTypeable #-} | ||
2 | {-# LANGUAGE DeriveFoldable #-} | ||
3 | {-# LANGUAGE DeriveFunctor #-} | ||
4 | {-# LANGUAGE DeriveGeneric #-} | ||
5 | {-# LANGUAGE DeriveTraversable #-} | ||
6 | {-# LANGUAGE FlexibleInstances #-} | ||
7 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} | ||
8 | {-# LANGUAGE PatternSynonyms #-} | ||
9 | module Tox where | ||
10 | |||
11 | import Control.Arrow | ||
12 | import qualified Crypto.Cipher.Salsa as Salsa | ||
13 | import qualified Crypto.Cipher.XSalsa as XSalsa | ||
14 | import Crypto.ECC.Class | ||
15 | import qualified Crypto.Error as Cryptonite | ||
16 | import Crypto.Error.Types | ||
17 | import qualified Crypto.MAC.Poly1305 as Poly1305 | ||
18 | import Crypto.PubKey.Curve25519 | ||
19 | import Crypto.PubKey.ECC.Types | ||
20 | import Data.Bool | ||
21 | import Data.ByteArray as BA | ||
22 | import Data.ByteString (ByteString) | ||
23 | import Data.ByteString as B | ||
24 | import qualified Data.ByteString.Base16 as Base16 | ||
25 | import qualified Data.ByteString.Char8 as C8 | ||
26 | import Data.ByteString.Lazy (toStrict) | ||
27 | import Data.Data | ||
28 | import Data.IP | ||
29 | import qualified Data.Serialize as S | ||
30 | import Data.Typeable | ||
31 | import Data.Word | ||
32 | import GHC.Generics (Generic) | ||
33 | import Network.Address (Address, fromSockAddr, sockAddrPort, | ||
34 | toSockAddr, withPort) | ||
35 | import Network.QueryResponse | ||
36 | import Network.Socket | ||
37 | import Data.Monoid | ||
38 | |||
39 | newtype NodeId = NodeId ByteString | ||
40 | deriving (Eq,Ord,Show,ByteArrayAccess) | ||
41 | |||
42 | instance S.Serialize NodeId where | ||
43 | get = NodeId <$> S.getBytes 32 | ||
44 | put (NodeId bs) = S.putByteString bs | ||
45 | |||
46 | data NodeInfo = NodeInfo | ||
47 | { nodeId :: NodeId | ||
48 | , nodeIP :: IP | ||
49 | , nodePort :: PortNumber | ||
50 | } | ||
51 | |||
52 | nodeAddr :: NodeInfo -> SockAddr | ||
53 | nodeAddr (NodeInfo _ ip port) = toSockAddr ip `withPort` port | ||
54 | |||
55 | nodeInfo :: NodeId -> SockAddr -> Either String NodeInfo | ||
56 | nodeInfo nid saddr | ||
57 | | Just ip <- fromSockAddr saddr | ||
58 | , Just port <- sockAddrPort saddr = Right $ NodeInfo nid ip port | ||
59 | | otherwise = Left "Address family not supported." | ||
60 | |||
61 | type TransactionId = Nonce8 | ||
62 | -- TODO | ||
63 | -- data TransactionId = TransactionId | ||
64 | -- { transactionKey :: Nonce8 -- ^ Used to lookup pending query. | ||
65 | -- , cryptoNonce :: Nonce24 -- ^ Used during encryption and decryption layer. | ||
66 | -- } | ||
67 | -- | ||
68 | -- Ensure that cryptoNonce is ignored by 'TableMethods' | ||
69 | |||
70 | newtype Method = MessageType Word8 | ||
71 | deriving (Eq, Ord, S.Serialize) | ||
72 | |||
73 | pattern PingType = MessageType 0 | ||
74 | pattern PongType = MessageType 1 | ||
75 | pattern GetNodesType = MessageType 2 | ||
76 | pattern SendNodesType = MessageType 4 | ||
77 | |||
78 | instance Show Method where | ||
79 | showsPrec d PingType = mappend "PingType" | ||
80 | showsPrec d PongType = mappend "PongType" | ||
81 | showsPrec d GetNodesType = mappend "GetNodesType" | ||
82 | showsPrec d SendNodesType = mappend "SendNodesType" | ||
83 | showsPrec d (MessageType x) = mappend "MessageType " . showsPrec (d+1) x | ||
84 | |||
85 | -- XXX: Possibly Word64 would be a better implementation. | ||
86 | newtype Nonce8 = Nonce8 ByteString | ||
87 | deriving (Eq, Ord, ByteArrayAccess) | ||
88 | |||
89 | instance Show Nonce8 where | ||
90 | showsPrec d nonce = quoted (mappend $ bin2hex nonce) | ||
91 | |||
92 | newtype Nonce24 = Nonce24 ByteString | ||
93 | deriving (Eq, Ord, ByteArrayAccess) | ||
94 | |||
95 | instance Show Nonce24 where | ||
96 | showsPrec d nonce = quoted (mappend $ bin2hex nonce) | ||
97 | |||
98 | instance S.Serialize Nonce24 where | ||
99 | get = Nonce24 <$> S.getBytes 24 | ||
100 | put (Nonce24 bs) = S.putByteString bs | ||
101 | |||
102 | quoted :: ShowS -> ShowS | ||
103 | quoted shows s = '"':shows ('"':s) | ||
104 | |||
105 | bin2hex :: ByteArrayAccess bs => bs -> String | ||
106 | bin2hex = C8.unpack . Base16.encode . convert | ||
107 | |||
108 | |||
109 | data Message a = Message | ||
110 | { msgType :: Method | ||
111 | , msgOrigin :: NodeId | ||
112 | , msgNonce :: Nonce24 | ||
113 | , msgPayload :: a | ||
114 | } | ||
115 | deriving (Eq, Show, Generic, Functor, Foldable, Traversable) | ||
116 | |||
117 | data Ciphered = Ciphered { cipheredMAC :: Poly1305.Auth | ||
118 | , cipheredBytes :: ByteString } | ||
119 | deriving Eq | ||
120 | |||
121 | getMessage :: S.Get (Message Ciphered) | ||
122 | getMessage = do | ||
123 | typ <- S.get | ||
124 | nid <- S.get | ||
125 | tid <- S.get | ||
126 | mac <- Poly1305.Auth . convert <$> S.getBytes 16 | ||
127 | cnt <- S.remaining | ||
128 | bs <- S.getBytes cnt | ||
129 | return Message { msgType = typ | ||
130 | , msgOrigin = nid | ||
131 | , msgNonce = tid | ||
132 | , msgPayload = Ciphered mac bs } | ||
133 | |||
134 | putMessage :: Message Ciphered -> S.Put | ||
135 | putMessage (Message {..}) = do | ||
136 | S.put msgType | ||
137 | S.put msgOrigin | ||
138 | S.put msgNonce | ||
139 | let Ciphered (Poly1305.Auth mac) bs = msgPayload | ||
140 | S.putByteString (convert mac) | ||
141 | S.putByteString bs | ||
142 | |||
143 | -- TODO: Cache symmetric keys. | ||
144 | data SecretsCache = SecretsCache | ||
145 | newEmptyCache = return SecretsCache | ||
146 | |||
147 | id2key :: NodeId -> PublicKey | ||
148 | id2key recipient = case publicKey recipient of | ||
149 | CryptoPassed key -> key | ||
150 | -- This should never happen because a NodeId is 32 bytes. | ||
151 | CryptoFailed e -> error ("Unexpected pattern fail: "++show e) | ||
152 | |||
153 | zeros32 :: Bytes | ||
154 | zeros32 = BA.replicate 32 0 | ||
155 | |||
156 | zeros24 :: Bytes | ||
157 | zeros24 = BA.take 24 zeros32 | ||
158 | |||
159 | hsalsa20 k n = a <> b | ||
160 | where | ||
161 | Salsa.State st = XSalsa.initialize 20 k n | ||
162 | (_, as) = BA.splitAt 4 st | ||
163 | (a, xs) = BA.splitAt 16 as | ||
164 | (_, bs) = BA.splitAt 24 xs | ||
165 | (b, _ ) = BA.splitAt 16 bs | ||
166 | |||
167 | |||
168 | computeSharedSecret :: SecretKey -> NodeId -> Nonce24 -> (Poly1305.State, XSalsa.State) | ||
169 | computeSharedSecret sk recipient nonce = (hash, crypt) | ||
170 | where | ||
171 | -- diffie helman | ||
172 | shared = ecdh (Proxy :: Proxy Curve_X25519) sk (id2key recipient) | ||
173 | -- shared secret XSalsa key | ||
174 | k = hsalsa20 shared zeros24 | ||
175 | -- cipher state | ||
176 | st0 = XSalsa.initialize 20 k nonce | ||
177 | -- Poly1305 key | ||
178 | (rs, crypt) = XSalsa.combine st0 zeros32 | ||
179 | -- Since rs is 32 bytes, this pattern should never fail... | ||
180 | Cryptonite.CryptoPassed hash = Poly1305.initialize rs | ||
181 | |||
182 | |||
183 | encryptMessage :: SecretKey -> SecretsCache -> NodeId -> Message ByteString -> Message Ciphered | ||
184 | encryptMessage sk _ recipient plaintext | ||
185 | = withSecret encipherAndHash sk recipient (msgNonce plaintext) <$> plaintext | ||
186 | |||
187 | decryptMessage :: SecretKey -> SecretsCache -> Message Ciphered -> Either String (Message ByteString) | ||
188 | decryptMessage sk _ ciphertext | ||
189 | = mapM (withSecret decipherAndAuth sk (msgOrigin ciphertext) (msgNonce ciphertext)) ciphertext | ||
190 | |||
191 | withSecret f sk recipient nonce x = f hash crypt x | ||
192 | where | ||
193 | (hash, crypt) = computeSharedSecret sk recipient nonce | ||
194 | |||
195 | |||
196 | encipherAndHash :: Poly1305.State -> XSalsa.State -> ByteString -> Ciphered | ||
197 | encipherAndHash hash crypt m = Ciphered a c | ||
198 | where | ||
199 | c = fst . XSalsa.combine crypt $ m | ||
200 | a = Poly1305.finalize . Poly1305.update hash $ c | ||
201 | |||
202 | decipherAndAuth :: Poly1305.State -> XSalsa.State -> Ciphered -> Either String ByteString | ||
203 | decipherAndAuth hash crypt (Ciphered mac c) | ||
204 | | (a == mac) = Right m | ||
205 | | otherwise = Left "decipherAndAuth: auth fail" | ||
206 | where | ||
207 | m = fst . XSalsa.combine crypt $ c | ||
208 | a = Poly1305.finalize . Poly1305.update hash $ c | ||
209 | |||
210 | |||
211 | parsePacket :: SecretKey -> SecretsCache -> ByteString -> SockAddr -> Either String (Message ByteString, NodeInfo) | ||
212 | parsePacket sk cache bs addr = do ciphered <- S.runGet getMessage bs | ||
213 | msg <- decryptMessage sk cache ciphered | ||
214 | ni <- nodeInfo (msgOrigin msg) addr | ||
215 | return (msg, ni) | ||
216 | |||
217 | encodePacket :: SecretKey -> SecretsCache -> Message ByteString -> NodeInfo -> (ByteString, SockAddr) | ||
218 | encodePacket sk cache msg ni = ( S.runPut . putMessage $ encryptMessage sk cache (nodeId ni) msg | ||
219 | , nodeAddr ni ) | ||
220 | |||
221 | newClient addr = do | ||
222 | udp <- udpTransport addr | ||
223 | secret <- generateSecretKey | ||
224 | cache <- newEmptyCache | ||
225 | let net = layerTransport (parsePacket secret cache) (encodePacket secret cache) udp | ||
226 | return net | ||
227 | |||
228 | last8 :: ByteString -> Nonce8 | ||
229 | last8 bs | ||
230 | | let len = B.length bs | ||
231 | , (len >= 8) = Nonce8 $ B.drop (len - 8) bs | ||
232 | | otherwise = Nonce8 $ B.replicate 8 0 | ||
233 | |||
234 | classify :: Message ByteString -> MessageClass String Method TransactionId | ||
235 | classify (Message { msgType = typ, msgPayload = bs }) = cls (last8 bs) | ||
236 | where | ||
237 | cls = case typ of | ||
238 | PingType -> IsQuery PingType | ||
239 | GetNodesType -> IsQuery GetNodesType | ||
240 | PongType -> IsResponse | ||
241 | SendNodesType -> IsResponse | ||
242 | |||
243 | encodePayload typ _ (Nonce8 tid) self dest b | ||
244 | = Message { msgType = typ | ||
245 | , msgOrigin = nodeId self | ||
246 | , msgNonce = error "encodePayload" | ||
247 | , msgPayload = S.encode b <> tid | ||
248 | } | ||
249 | |||
250 | decodePayload :: S.Serialize a => Message ByteString -> Either String a | ||
251 | decodePayload msg = S.decode $ msgPayload msg | ||
252 | |||
253 | handler typ f = Just $ MethodHandler decodePayload (encodePayload typ) f | ||
254 | |||
255 | handlers :: Method -> Maybe (MethodHandler String TransactionId NodeInfo (Message ByteString) ()) | ||
256 | handlers PingType = handler PingType pingH | ||
257 | handlers GetNodesType = error "find_node" | ||
258 | handlers _ = Nothing | ||
259 | |||
260 | data Ping = Ping | ||
261 | |||
262 | pingH :: NodeInfo -> Ping -> IO Ping | ||
263 | pingH = error "pingH" | ||