Fixed bugs, local communication HMAC added in
[iotcloud.git] / version2 / src / java / iotcloud / CloudComm.java
1 package iotcloud;
2
3 import java.io.*;
4 import java.net.*;
5 import java.util.Arrays;
6 import javax.crypto.*;
7 import javax.crypto.spec.*;
8 import java.security.SecureRandom;
9
10 /**
11  * This class provides a communication API to the webserver.  It also
12  * validates the HMACs on the slots and handles encryption.
13  * @author Brian Demsky <bdemsky@uci.edu>
14  * @version 1.0
15  */
16
17
18 class CloudComm {
19         private static final int SALT_SIZE = 8;
20         private static final int TIMEOUT_MILLIS = 25; // 100
21
22         /** Sets the size for the HMAC. */
23         static final int HMAC_SIZE = 32;
24
25         private String baseurl;
26         private Cipher encryptCipher;
27         private Cipher decryptCipher;
28         private Mac mac;
29         private String password;
30         private SecureRandom random;
31         private byte salt[];
32         private Table table;
33         private int listeningPort = -1;
34         private Thread localServerThread = null;
35         private boolean doEnd = false;
36
37         /**
38          * Empty Constructor needed for child class.
39          */
40         CloudComm() {
41         }
42
43         /**
44          * Constructor for actual use. Takes in the url and password.
45          */
46         CloudComm(Table _table,  String _baseurl, String _password, int _listeningPort) {
47                 this.table = _table;
48                 this.baseurl = _baseurl;
49                 this.password = _password;
50                 this.random = new SecureRandom();
51                 this.listeningPort = _listeningPort;
52
53                 if (this.listeningPort > 0) {
54                         localServerThread = new Thread(new Runnable() {
55                                 public void run() {
56                                         localServerWorkerFunction();
57                                 }
58                         });
59                         localServerThread.start();
60                 }
61         }
62
63         /**
64          * Generates Key from password.
65          */
66         private SecretKeySpec initKey() {
67                 try {
68                         PBEKeySpec keyspec = new PBEKeySpec(password.toCharArray(),
69                                                             salt,
70                                                             65536,
71                                                             128);
72                         SecretKey tmpkey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(keyspec);
73                         return new SecretKeySpec(tmpkey.getEncoded(), "AES");
74                 } catch (Exception e) {
75                         e.printStackTrace();
76                         throw new Error("Failed generating key.");
77                 }
78         }
79
80         /**
81          * Inits all the security stuff
82          */
83         public void initSecurity() throws ServerException {
84                 // try to get the salt and if one does not exist set one
85                 if (!getSalt()) {
86                         //Set the salt
87                         setSalt();
88                 }
89
90                 initCrypt();
91         }
92
93         /**
94          * Inits the HMAC generator.
95          */
96         private void initCrypt() {
97
98                 if (password == null) {
99                         return;
100                 }
101
102                 try {
103                         SecretKeySpec key = initKey();
104                         password = null; // drop password
105                         mac = Mac.getInstance("HmacSHA256");
106                         mac.init(key);
107                         encryptCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
108                         encryptCipher.init(Cipher.ENCRYPT_MODE, key);
109                         decryptCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
110                         decryptCipher.init(Cipher.DECRYPT_MODE, key);
111                 } catch (Exception e) {
112                         e.printStackTrace();
113                         throw new Error("Failed To Initialize Ciphers");
114                 }
115         }
116
117         /*
118          * Builds the URL for the given request.
119          */
120         private URL buildRequest(boolean isput, long sequencenumber, long maxentries) throws IOException {
121                 String reqstring = isput ? "req=putslot" : "req=getslot";
122                 String urlstr = baseurl + "?" + reqstring + "&seq=" + sequencenumber;
123                 if (maxentries != 0)
124                         urlstr += "&max=" + maxentries;
125                 return new URL(urlstr);
126         }
127
128         private void setSalt() throws ServerException {
129
130                 if (salt != null) {
131                         // Salt already sent to server so dont set it again
132                         return;
133                 }
134
135                 try {
136                         byte[] saltTmp = new byte[SALT_SIZE];
137                         random.nextBytes(saltTmp);
138
139                         URL url = new URL(baseurl + "?req=setsalt");
140                         URLConnection con = url.openConnection();
141                         HttpURLConnection http = (HttpURLConnection) con;
142
143                         http.setRequestMethod("POST");
144                         http.setFixedLengthStreamingMode(saltTmp.length);
145                         http.setDoOutput(true);
146                         http.setConnectTimeout(TIMEOUT_MILLIS);
147                         http.connect();
148
149                         OutputStream os = http.getOutputStream();
150                         os.write(saltTmp);
151                         os.flush();
152
153                         int responsecode = http.getResponseCode();
154                         if (responsecode != HttpURLConnection.HTTP_OK) {
155                                 // TODO: Remove this print
156                                 System.out.println(responsecode);
157                                 throw new Error("Invalid response");
158                         }
159
160                         salt = saltTmp;
161                 } catch (Exception e) {
162                         // e.printStackTrace();
163                         throw new ServerException("Failed setting salt", ServerException.TypeConnectTimeout);
164                 }
165         }
166
167         private boolean getSalt() throws ServerException {
168                 URL url = null;
169                 URLConnection con = null;
170                 HttpURLConnection http = null;
171
172                 try {
173                         url = new URL(baseurl + "?req=getsalt");
174                 } catch (Exception e) {
175                         // e.printStackTrace();
176                         throw new Error("getSlot failed");
177                 }
178                 try {
179
180                         con = url.openConnection();
181                         http = (HttpURLConnection) con;
182                         http.setRequestMethod("POST");
183                         http.setConnectTimeout(TIMEOUT_MILLIS);
184                         http.setReadTimeout(TIMEOUT_MILLIS);
185                         http.connect();
186                 } catch (SocketTimeoutException e) {
187                         throw new ServerException("getSalt failed", ServerException.TypeConnectTimeout);
188                 } catch (Exception e) {
189                         // e.printStackTrace();
190                         throw new Error("getSlot failed");
191                 }
192
193                 try {
194
195                         int responsecode = http.getResponseCode();
196                         if (responsecode != HttpURLConnection.HTTP_OK) {
197                                 // TODO: Remove this print
198                                 // System.out.println(responsecode);
199                                 throw new Error("Invalid response");
200                         }
201
202                         InputStream is = http.getInputStream();
203                         if (is.available() > 0) {
204                                 DataInputStream dis = new DataInputStream(is);
205                                 int salt_length = dis.readInt();
206                                 byte [] tmp = new byte[salt_length];
207                                 dis.readFully(tmp);
208                                 salt = tmp;
209                                 return true;
210                         } else {
211                                 return false;
212                         }
213                 } catch (SocketTimeoutException e) {
214                         throw new ServerException("getSalt failed", ServerException.TypeInputTimeout);
215                 } catch (Exception e) {
216                         // e.printStackTrace();
217                         throw new Error("getSlot failed");
218                 }
219         }
220
221         /*
222          * API for putting a slot into the queue.  Returns null on success.
223          * On failure, the server will send slots with newer sequence
224          * numbers.
225          */
226         public Slot[] putSlot(Slot slot, int max) throws ServerException {
227                 URL url = null;
228                 URLConnection con = null;
229                 HttpURLConnection http = null;
230
231                 try {
232                         if (salt == null) {
233                                 if (!getSalt()) {
234                                         throw new ServerException("putSlot failed", ServerException.TypeSalt);
235                                 }
236                                 initCrypt();
237                         }
238
239                         long sequencenumber = slot.getSequenceNumber();
240                         byte[] bytes = slot.encode(mac);
241                         bytes = encryptCipher.doFinal(bytes);
242
243                         url = buildRequest(true, sequencenumber, max);
244                         con = url.openConnection();
245                         http = (HttpURLConnection) con;
246
247                         http.setRequestMethod("POST");
248                         http.setFixedLengthStreamingMode(bytes.length);
249                         http.setDoOutput(true);
250                         http.setConnectTimeout(TIMEOUT_MILLIS);
251                         http.setReadTimeout(TIMEOUT_MILLIS);
252                         http.connect();
253
254                         OutputStream os = http.getOutputStream();
255                         os.write(bytes);
256                         os.flush();
257
258                         // System.out.println("Bytes Sent: " + bytes.length);
259                 } catch (ServerException e) {
260                         throw e;
261                 } catch (SocketTimeoutException e) {
262                         throw new ServerException("putSlot failed", ServerException.TypeConnectTimeout);
263                 } catch (Exception e) {
264                         // e.printStackTrace();
265                         throw new Error("putSlot failed");
266                 }
267
268
269
270                 try {
271                         InputStream is = http.getInputStream();
272                         DataInputStream dis = new DataInputStream(is);
273                         byte[] resptype = new byte[7];
274                         dis.readFully(resptype);
275
276                         if (Arrays.equals(resptype, "getslot".getBytes()))
277                                 return processSlots(dis);
278                         else if (Arrays.equals(resptype, "putslot".getBytes()))
279                                 return null;
280                         else
281                                 throw new Error("Bad response to putslot");
282
283                 } catch (SocketTimeoutException e) {
284                         throw new ServerException("putSlot failed", ServerException.TypeInputTimeout);
285                 } catch (Exception e) {
286                         // e.printStackTrace();
287                         throw new Error("putSlot failed");
288                 }
289         }
290
291         /**
292          * Request the server to send all slots with the given
293          * sequencenumber or newer.
294          */
295         public Slot[] getSlots(long sequencenumber) throws ServerException {
296                 URL url = null;
297                 URLConnection con = null;
298                 HttpURLConnection http = null;
299
300                 try {
301                         if (salt == null) {
302                                 if (!getSalt()) {
303                                         throw new ServerException("getSlots failed", ServerException.TypeSalt);
304                                 }
305                                 initCrypt();
306                         }
307
308                         url = buildRequest(false, sequencenumber, 0);
309                         con = url.openConnection();
310                         http = (HttpURLConnection) con;
311                         http.setRequestMethod("POST");
312                         http.setConnectTimeout(TIMEOUT_MILLIS);
313                         http.setReadTimeout(TIMEOUT_MILLIS);
314                         http.connect();
315                 } catch (SocketTimeoutException e) {
316                         throw new ServerException("getSlots failed", ServerException.TypeConnectTimeout);
317                 } catch (ServerException e) {
318                         throw e;
319                 } catch (Exception e) {
320                         // e.printStackTrace();
321                         throw new Error("getSlots failed");
322                 }
323
324                 try {
325                         InputStream is = http.getInputStream();
326                         DataInputStream dis = new DataInputStream(is);
327                         byte[] resptype = new byte[7];
328                         dis.readFully(resptype);
329                         if (!Arrays.equals(resptype, "getslot".getBytes()))
330                                 throw new Error("Bad Response: " + new String(resptype));
331                         else
332                                 return processSlots(dis);
333                 } catch (SocketTimeoutException e) {
334                         throw new ServerException("getSlots failed", ServerException.TypeInputTimeout);
335                 } catch (Exception e) {
336                         // e.printStackTrace();
337                         throw new Error("getSlots failed");
338                 }
339         }
340
341         /**
342          * Method that actually handles building Slot objects from the
343          * server response.  Shared by both putSlot and getSlots.
344          */
345         private Slot[] processSlots(DataInputStream dis) throws Exception {
346                 int numberofslots = dis.readInt();
347                 int[] sizesofslots = new int[numberofslots];
348
349                 Slot[] slots = new Slot[numberofslots];
350                 for (int i = 0; i < numberofslots; i++)
351                         sizesofslots[i] = dis.readInt();
352
353                 for (int i = 0; i < numberofslots; i++) {
354
355                         byte[] data = new byte[sizesofslots[i]];
356                         dis.readFully(data);
357
358                         data = decryptCipher.doFinal(data);
359
360                         slots[i] = Slot.decode(table, data, mac);
361                 }
362                 dis.close();
363                 return slots;
364         }
365
366         public byte[] sendLocalData(byte[] sendData, String host, int port) {
367
368                 if (salt == null) {
369                         return null;
370                 }
371                 try {
372
373                         System.out.println("Passing Locally");
374
375                         mac.update(sendData);
376                         byte[] genmac = mac.doFinal();
377                         byte[] totalData = new byte[sendData.length + genmac.length];
378                         System.arraycopy(sendData, 0, totalData, 0, sendData.length);
379                         System.arraycopy(genmac, 0, totalData, sendData.length, genmac.length);
380
381                         // Encrypt the data for sending
382                         // byte[] encryptedData = encryptCipher.doFinal(totalData);
383                         byte[] encryptedData = encryptCipher.doFinal(totalData);
384
385                         // Open a TCP socket connection to a local device
386                         Socket socket = new Socket(host, port);
387                         socket.setReuseAddress(true);
388                         DataOutputStream output = new DataOutputStream(socket.getOutputStream());
389                         DataInputStream input = new DataInputStream(socket.getInputStream());
390
391                         // Send data to output (length of data, the data)
392                         output.writeInt(encryptedData.length);
393                         output.write(encryptedData, 0, encryptedData.length);
394                         output.flush();
395
396                         int lengthOfReturnData = input.readInt();
397                         byte[] returnData = new byte[lengthOfReturnData];
398                         input.readFully(returnData);
399                         returnData = decryptCipher.doFinal(returnData);
400
401                         // We are done with this socket
402                         socket.close();
403
404                         mac.update(returnData, 0, returnData.length - HMAC_SIZE);
405                         byte[] realmac = mac.doFinal();
406                         byte[] recmac = new byte[HMAC_SIZE];
407                         System.arraycopy(returnData, returnData.length - realmac.length, recmac, 0, realmac.length);
408
409                         if (!Arrays.equals(recmac, realmac))
410                                 throw new Error("Local Error: Invalid HMAC!  Potential Attack!");
411
412                         byte[] returnData2 = new byte[lengthOfReturnData - recmac.length];
413                         System.arraycopy(returnData, 0, returnData2, 0, returnData2.length);
414
415                         return returnData2;
416                 } catch (SocketTimeoutException e) {
417
418                 } catch (BadPaddingException e) {
419
420                 } catch (IllegalBlockSizeException e) {
421
422                 } catch (UnknownHostException e) {
423
424                 } catch (IOException e) {
425
426                 }
427
428                 return null;
429         }
430
431         private void localServerWorkerFunction() {
432
433                 ServerSocket inputSocket = null;
434
435                 try {
436                         // Local server socket
437                         inputSocket = new ServerSocket(listeningPort);
438                         inputSocket.setReuseAddress(true);
439                         inputSocket.setSoTimeout(TIMEOUT_MILLIS);
440                 } catch (Exception e) {
441                         e.printStackTrace();
442                         throw new Error("Local server setup failure...");
443                 }
444
445                 while (!doEnd) {
446
447                         try {
448                                 // Accept incoming socket
449                                 Socket socket = inputSocket.accept();
450
451                                 DataInputStream input = new DataInputStream(socket.getInputStream());
452                                 DataOutputStream output = new DataOutputStream(socket.getOutputStream());
453
454                                 // Get the encrypted data from the server
455                                 int dataSize = input.readInt();
456                                 byte[] readData = new byte[dataSize];
457                                 input.readFully(readData);
458
459                                 // Decrypt the data
460                                 readData = decryptCipher.doFinal(readData);
461
462                                 mac.update(readData, 0, readData.length - HMAC_SIZE);
463                                 byte[] genmac = mac.doFinal();
464                                 byte[] recmac = new byte[HMAC_SIZE];
465                                 System.arraycopy(readData, readData.length - recmac.length, recmac, 0, recmac.length);
466
467                                 if (!Arrays.equals(recmac, genmac))
468                                         throw new Error("Local Error: Invalid HMAC!  Potential Attack!");
469
470                                 byte[] returnData = new byte[readData.length - recmac.length];
471                                 System.arraycopy(readData, 0, returnData, 0, returnData.length);
472
473                                 // Process the data
474                                 // byte[] sendData = table.acceptDataFromLocal(readData);
475                                 byte[] sendData = table.acceptDataFromLocal(returnData);
476
477                                 mac.update(sendData);
478                                 byte[] realmac = mac.doFinal();
479                                 byte[] totalData = new byte[sendData.length + realmac.length];
480                                 System.arraycopy(sendData, 0, totalData, 0, sendData.length);
481                                 System.arraycopy(realmac, 0, totalData, sendData.length, realmac.length);
482
483                                 // Encrypt the data for sending
484                                 byte[] encryptedData = encryptCipher.doFinal(totalData);
485
486                                 // Send data to output (length of data, the data)
487                                 output.writeInt(encryptedData.length);
488                                 output.write(encryptedData, 0, encryptedData.length);
489                                 output.flush();
490
491                                 // close the socket
492                                 socket.close();
493                         } catch (SocketTimeoutException e) {
494
495                         } catch (BadPaddingException e) {
496
497                         } catch (IllegalBlockSizeException e) {
498
499                         } catch (UnknownHostException e) {
500
501                         } catch (IOException e) {
502
503                         }
504                 }
505
506                 if (inputSocket != null) {
507                         try {
508                                 inputSocket.close();
509                         } catch (Exception e) {
510                                 e.printStackTrace();
511                                 throw new Error("Local server close failure...");
512                         }
513                 }
514         }
515
516         public void close() {
517                 doEnd = true;
518
519                 if (localServerThread != null) {
520                         try {
521                                 localServerThread.join();
522                         } catch (Exception e) {
523                                 e.printStackTrace();
524                                 throw new Error("Local Server thread join issue...");
525                         }
526                 }
527
528                 // System.out.println("Done Closing Cloud Comm");
529         }
530
531         protected void finalize() throws Throwable {
532                 try {
533                         close();        // close open files
534                 } finally {
535                         super.finalize();
536                 }
537         }
538
539 }