Changed AES to CTR mode
[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                 buffer.putLong(localSequenceNumber);
251                 return buffer.array();
252
253         }
254
255         private byte[] encryptSlotAndPrependIV(byte[] rawData, byte[] ivBytes) {
256                 try {
257                         IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
258                         Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5Padding");
259                         cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
260
261                         byte[] encryptedBytes = cipher.doFinal(rawData);
262
263                         byte[] bytes = new byte[encryptedBytes.length + IV_SIZE];
264                         System.arraycopy(ivBytes, 0, bytes, 0, ivBytes.length);
265                         System.arraycopy(encryptedBytes, 0, bytes, IV_SIZE, encryptedBytes.length);
266
267                         return bytes;
268
269                 } catch (Exception e) {
270                         e.printStackTrace();
271                         throw new Error("Failed To Encrypt");
272                 }
273         }
274
275
276         private byte[] stripIVAndDecryptSlot(byte[] rawData) {
277                 try {
278                         byte[] ivBytes = new byte[IV_SIZE];
279                         byte[] encryptedBytes = new byte[rawData.length - IV_SIZE];
280                         System.arraycopy(rawData, 0, ivBytes, 0, IV_SIZE);
281                         System.arraycopy(rawData, IV_SIZE, encryptedBytes, 0 , encryptedBytes.length);
282
283                         IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
284
285                         Cipher cipher = Cipher.getInstance("AES/CTR/PKCS5Padding");
286                         cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
287
288                         return cipher.doFinal(encryptedBytes);
289
290                 } catch (Exception e) {
291                         e.printStackTrace();
292                         throw new Error("Failed To Decrypt");
293                 }
294         }
295
296
297         /*
298          * API for putting a slot into the queue.  Returns null on success.
299          * On failure, the server will send slots with newer sequence
300          * numbers.
301          */
302         public Slot[] putSlot(Slot slot, int max) throws ServerException {
303                 URL url = null;
304                 URLConnection con = null;
305                 HttpURLConnection http = null;
306
307                 try {
308                         if (salt == null) {
309                                 if (!getSalt()) {
310                                         throw new ServerException("putSlot failed", ServerException.TypeSalt);
311                                 }
312                                 initCrypt();
313                         }
314
315                         long sequencenumber = slot.getSequenceNumber();
316                         byte[] slotBytes = slot.encode(mac);
317                         // slotBytes = encryptCipher.doFinal(slotBytes);
318
319                         // byte[] iVBytes = slot.getSlotCryptIV();
320
321                         // byte[] bytes = new byte[slotBytes.length + IV_SIZE];
322                         // System.arraycopy(iVBytes, 0, bytes, 0, iVBytes.length);
323                         // System.arraycopy(slotBytes, 0, bytes, IV_SIZE, slotBytes.length);
324
325
326                         byte[] bytes = encryptSlotAndPrependIV(slotBytes, slot.getSlotCryptIV());
327
328                         url = buildRequest(true, sequencenumber, max);
329
330                         timer.startTime();
331                         con = url.openConnection();
332                         http = (HttpURLConnection) con;
333
334                         http.setRequestMethod("POST");
335                         http.setFixedLengthStreamingMode(bytes.length);
336                         http.setDoOutput(true);
337                         http.setConnectTimeout(TIMEOUT_MILLIS);
338                         http.setReadTimeout(TIMEOUT_MILLIS);
339                         http.connect();
340
341                         OutputStream os = http.getOutputStream();
342                         os.write(bytes);
343                         os.flush();
344
345                         timer.endTime();
346
347
348                         // System.out.println("Bytes Sent: " + bytes.length);
349                 } catch (ServerException e) {
350                         timer.endTime();
351
352                         throw e;
353                 } catch (SocketTimeoutException e) {
354                         timer.endTime();
355
356                         throw new ServerException("putSlot failed", ServerException.TypeConnectTimeout);
357                 } catch (Exception e) {
358                         // e.printStackTrace();
359                         throw new Error("putSlot failed");
360                 }
361
362
363
364                 try {
365                         timer.startTime();
366                         InputStream is = http.getInputStream();
367                         DataInputStream dis = new DataInputStream(is);
368                         byte[] resptype = new byte[7];
369                         dis.readFully(resptype);
370                         timer.endTime();
371
372                         if (Arrays.equals(resptype, "getslot".getBytes())) {
373                                 return processSlots(dis);
374                         } else if (Arrays.equals(resptype, "putslot".getBytes())) {
375                                 return null;
376                         } else
377                                 throw new Error("Bad response to putslot");
378
379                 } catch (SocketTimeoutException e) {
380                         timer.endTime();
381                         throw new ServerException("putSlot failed", ServerException.TypeInputTimeout);
382                 } catch (Exception e) {
383                         // e.printStackTrace();
384                         throw new Error("putSlot failed");
385                 }
386         }
387
388         /**
389          * Request the server to send all slots with the given
390          * sequencenumber or newer.
391          */
392         public Slot[] getSlots(long sequencenumber) throws ServerException {
393                 URL url = null;
394                 URLConnection con = null;
395                 HttpURLConnection http = null;
396
397                 try {
398                         if (salt == null) {
399                                 if (!getSalt()) {
400                                         throw new ServerException("getSlots failed", ServerException.TypeSalt);
401                                 }
402                                 initCrypt();
403                         }
404
405                         url = buildRequest(false, sequencenumber, 0);
406                         timer.startTime();
407                         con = url.openConnection();
408                         http = (HttpURLConnection) con;
409                         http.setRequestMethod("POST");
410                         http.setConnectTimeout(TIMEOUT_MILLIS);
411                         http.setReadTimeout(TIMEOUT_MILLIS);
412
413
414
415                         http.connect();
416                         timer.endTime();
417
418                 } catch (SocketTimeoutException e) {
419                         timer.endTime();
420
421                         throw new ServerException("getSlots failed", ServerException.TypeConnectTimeout);
422                 } catch (ServerException e) {
423                         timer.endTime();
424
425                         throw e;
426                 } catch (Exception e) {
427                         // e.printStackTrace();
428                         throw new Error("getSlots failed");
429                 }
430
431                 try {
432
433                         timer.startTime();
434                         InputStream is = http.getInputStream();
435                         DataInputStream dis = new DataInputStream(is);
436                         byte[] resptype = new byte[7];
437
438                         dis.readFully(resptype);
439                         timer.endTime();
440
441                         if (!Arrays.equals(resptype, "getslot".getBytes()))
442                                 throw new Error("Bad Response: " + new String(resptype));
443
444                         return processSlots(dis);
445                 } catch (SocketTimeoutException e) {
446                         timer.endTime();
447
448                         throw new ServerException("getSlots failed", ServerException.TypeInputTimeout);
449                 } catch (Exception e) {
450                         // e.printStackTrace();
451                         throw new Error("getSlots failed");
452                 }
453         }
454
455         /**
456          * Method that actually handles building Slot objects from the
457          * server response.  Shared by both putSlot and getSlots.
458          */
459         private Slot[] processSlots(DataInputStream dis) throws Exception {
460                 int numberofslots = dis.readInt();
461                 int[] sizesofslots = new int[numberofslots];
462
463                 Slot[] slots = new Slot[numberofslots];
464                 for (int i = 0; i < numberofslots; i++)
465                         sizesofslots[i] = dis.readInt();
466
467                 for (int i = 0; i < numberofslots; i++) {
468
469                         byte[] rawData = new byte[sizesofslots[i]];
470                         dis.readFully(rawData);
471
472
473                         // byte[] data = new byte[rawData.length - IV_SIZE];
474                         // System.arraycopy(rawData, IV_SIZE, data, 0, data.length);
475
476
477                         byte[] data = stripIVAndDecryptSlot(rawData);
478
479                         // data = decryptCipher.doFinal(data);
480
481                         slots[i] = Slot.decode(table, data, mac);
482                 }
483                 dis.close();
484                 return slots;
485         }
486
487         public byte[] sendLocalData(byte[] sendData, long localSequenceNumber, String host, int port) {
488
489                 if (salt == null) {
490                         return null;
491                 }
492                 try {
493                         System.out.println("Passing Locally");
494
495                         mac.update(sendData);
496                         byte[] genmac = mac.doFinal();
497                         byte[] totalData = new byte[sendData.length + genmac.length];
498                         System.arraycopy(sendData, 0, totalData, 0, sendData.length);
499                         System.arraycopy(genmac, 0, totalData, sendData.length, genmac.length);
500
501                         // Encrypt the data for sending
502                         // byte[] encryptedData = encryptCipher.doFinal(totalData);
503                         // byte[] encryptedData = encryptCipher.doFinal(totalData);
504                         byte[] iv = createIV(table.getMachineId(), table.getLocalSequenceNumber());
505                         byte[] encryptedData = encryptSlotAndPrependIV(totalData, iv);
506
507                         // Open a TCP socket connection to a local device
508                         Socket socket = new Socket(host, port);
509                         socket.setReuseAddress(true);
510                         DataOutputStream output = new DataOutputStream(socket.getOutputStream());
511                         DataInputStream input = new DataInputStream(socket.getInputStream());
512
513
514                         timer.startTime();
515                         // Send data to output (length of data, the data)
516                         output.writeInt(encryptedData.length);
517                         output.write(encryptedData, 0, encryptedData.length);
518                         output.flush();
519
520                         int lengthOfReturnData = input.readInt();
521                         byte[] returnData = new byte[lengthOfReturnData];
522                         input.readFully(returnData);
523
524                         timer.endTime();
525
526                         // returnData = decryptCipher.doFinal(returnData);
527                         returnData = stripIVAndDecryptSlot(returnData);
528                         // returnData = decryptCipher.doFinal(returnData);
529
530                         // We are done with this socket
531                         socket.close();
532
533                         mac.update(returnData, 0, returnData.length - HMAC_SIZE);
534                         byte[] realmac = mac.doFinal();
535                         byte[] recmac = new byte[HMAC_SIZE];
536                         System.arraycopy(returnData, returnData.length - realmac.length, recmac, 0, realmac.length);
537
538                         if (!Arrays.equals(recmac, realmac))
539                                 throw new Error("Local Error: Invalid HMAC!  Potential Attack!");
540
541                         byte[] returnData2 = new byte[lengthOfReturnData - recmac.length];
542                         System.arraycopy(returnData, 0, returnData2, 0, returnData2.length);
543
544                         return returnData2;
545                 } catch (Exception e) {
546                         e.printStackTrace();
547                         // throw new Error("Local comms failure...");
548
549                 }
550
551                 return null;
552         }
553
554         private void localServerWorkerFunction() {
555
556                 ServerSocket inputSocket = null;
557
558                 try {
559                         // Local server socket
560                         inputSocket = new ServerSocket(listeningPort);
561                         inputSocket.setReuseAddress(true);
562                         inputSocket.setSoTimeout(TIMEOUT_MILLIS);
563                 } catch (Exception e) {
564                         e.printStackTrace();
565                         throw new Error("Local server setup failure...");
566                 }
567
568                 while (!doEnd) {
569
570                         try {
571                                 // Accept incoming socket
572                                 Socket socket = inputSocket.accept();
573
574                                 DataInputStream input = new DataInputStream(socket.getInputStream());
575                                 DataOutputStream output = new DataOutputStream(socket.getOutputStream());
576
577                                 // Get the encrypted data from the server
578                                 int dataSize = input.readInt();
579                                 byte[] readData = new byte[dataSize];
580                                 input.readFully(readData);
581
582                                 timer.endTime();
583
584                                 // Decrypt the data
585                                 // readData = decryptCipher.doFinal(readData);
586                                 readData = stripIVAndDecryptSlot(readData);
587
588                                 mac.update(readData, 0, readData.length - HMAC_SIZE);
589                                 byte[] genmac = mac.doFinal();
590                                 byte[] recmac = new byte[HMAC_SIZE];
591                                 System.arraycopy(readData, readData.length - recmac.length, recmac, 0, recmac.length);
592
593                                 if (!Arrays.equals(recmac, genmac))
594                                         throw new Error("Local Error: Invalid HMAC!  Potential Attack!");
595
596                                 byte[] returnData = new byte[readData.length - recmac.length];
597                                 System.arraycopy(readData, 0, returnData, 0, returnData.length);
598
599                                 // Process the data
600                                 // byte[] sendData = table.acceptDataFromLocal(readData);
601                                 byte[] sendData = table.acceptDataFromLocal(returnData);
602
603
604                                 mac.update(sendData);
605                                 byte[] realmac = mac.doFinal();
606                                 byte[] totalData = new byte[sendData.length + realmac.length];
607                                 System.arraycopy(sendData, 0, totalData, 0, sendData.length);
608                                 System.arraycopy(realmac, 0, totalData, sendData.length, realmac.length);
609
610                                 // Encrypt the data for sending
611                                 // byte[] encryptedData = encryptCipher.doFinal(totalData);
612                                 byte[] iv = createIV(table.getMachineId(), table.getLocalSequenceNumber());
613                                 byte[] encryptedData = encryptSlotAndPrependIV(totalData, iv);
614
615
616                                 timer.startTime();
617                                 // Send data to output (length of data, the data)
618                                 output.writeInt(encryptedData.length);
619                                 output.write(encryptedData, 0, encryptedData.length);
620                                 output.flush();
621
622                                 // close the socket
623                                 socket.close();
624                         } catch (Exception e) {
625
626                         }
627                 }
628
629                 if (inputSocket != null) {
630                         try {
631                                 inputSocket.close();
632                         } catch (Exception e) {
633                                 e.printStackTrace();
634                                 throw new Error("Local server close failure...");
635                         }
636                 }
637         }
638
639         public void close() {
640                 doEnd = true;
641
642                 if (localServerThread != null) {
643                         try {
644                                 localServerThread.join();
645                         } catch (Exception e) {
646                                 e.printStackTrace();
647                                 throw new Error("Local Server thread join issue...");
648                         }
649                 }
650
651                 // System.out.println("Done Closing Cloud Comm");
652         }
653
654         protected void finalize() throws Throwable {
655                 try {
656                         close();        // close open files
657                 } finally {
658                         super.finalize();
659                 }
660         }
661 }