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