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