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