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