001 package edu.harvard.deas.hyperenc.vsat; 002 003 import java.io.ByteArrayOutputStream; 004 import java.io.File; 005 import java.io.FileFilter; 006 import java.io.FileInputStream; 007 import java.io.FileNotFoundException; 008 import java.io.IOException; 009 import java.util.Arrays; 010 import java.util.Random; 011 import java.util.zip.GZIPOutputStream; 012 013 import javax.crypto.Cipher; 014 import javax.crypto.IllegalBlockSizeException; 015 import javax.crypto.KeyGenerator; 016 import javax.crypto.SecretKey; 017 018 019 // TODO: Use stronger prng! 020 /** 021 * FileRandomSource (FRS) traverses the file system collecting random data. 022 * Then, it encrypts, compresses and then xor's the resulting pages to 023 * produce a final page to hand to the PSN 024 */ 025 public class FileRandomSource extends TrueRandomSource { 026 /** Controls debugging output. */ 027 private static final boolean DEBUG = false; 028 029 /** 030 * We will never descend further than MAX_DEPTH 031 */ 032 final static int MAX_DEPTH = 10; 033 034 /** 035 * Number of random pages to combine into the final random page 036 */ 037 public static final int NUM_PAGES = 2; 038 039 /** 040 * Root of the file system 041 */ 042 private File rootFile; 043 044 /** 045 * Buffer to hold accumulated file randomness 046 */ 047 private byte[] rfData; 048 049 /** 050 * Amount of data read thus far during file system traversal 051 */ 052 private int retrData = 0; 053 054 /** 055 * Local pseudo-random number generator 056 */ 057 private Random myRand; 058 059 /** 060 * Simple constructor to initialize components 061 */ 062 public FileRandomSource() 063 { 064 //create a new file at the root directory 065 rootFile = new File("/"); 066 067 // Initialize the prng 068 myRand = new Random(); 069 070 // Create buffer for accumulated file randomness 071 rfData = new byte[DATA_LENGTH]; 072 } 073 074 /** 075 * Pieces everything together: 076 * Gathers file data and then encrypts, compresses and xor's pages to return a page of random data 077 * Returns null if it encounters an error 078 */ 079 public byte [] genRandomness() { 080 byte[] result; 081 082 if (DEBUG) System.out.println("Grabbing file data"); 083 try { // General purpose error trap, so we can return null if we hit a problem 084 result = new byte[DATA_LENGTH]; 085 086 // Initialize result to 0 087 Arrays.fill(result, (byte) 0); 088 089 for (int i=0; i < NUM_PAGES; i++) { 090 byte[] page = new byte[DATA_LENGTH]; 091 byte[] data = null; 092 int amountRead = 0; 093 094 do { 095 // Grab a compressed, encrypted page 096 byte[] randdata = getRandomData(); 097 byte[] gzipdata = compressPage(randdata); 098 data = encryptPage(gzipdata); 099 if (data == null) 100 throw new RuntimeException("failed to produce data"); 101 102 // FIXME instead of encrypting first, and using those results to fill 103 // this array, we should fill it with compressed data, and then encrypt 104 // the result 105 // (that would avoid having to truncate the gzip output to a multiple 106 // of 64, or worse, having to zero-pad it) 107 108 // Since data may not be a full page copy over whatever we got 109 for (int j = amountRead; j < DATA_LENGTH && j - amountRead < data.length; j++) { 110 page[j] = data[j - amountRead]; 111 } 112 113 amountRead += data.length; 114 } while (amountRead < DATA_LENGTH); 115 116 result = xor(result, page); 117 } 118 } catch (Exception e) { 119 throw new RuntimeException(e); 120 //result = null; 121 } 122 123 return result; 124 } 125 126 127 128 /** 129 * Computes the byte-wise xor of page1 and page2 130 */ 131 public byte[] xor(byte[] page1, byte[] page2) { 132 int min = Math.min(page1.length, page2.length); 133 byte[] result = new byte[min]; 134 135 // xor the data to conceal info 136 for (int i = 0; i < min; i++) { 137 result[i] = (byte) (page1[i] ^ page2[i]); 138 } 139 140 return result; 141 } 142 143 144 /** 145 * Returns the result of encrypting the given page using a random key with Triple DES, 146 * or null if it encounters an error 147 * @param page The array of bytes to be encrypted 148 */ 149 public byte [] encryptPage(byte[] page) { 150 byte[] result = null; // Encrypted page 151 152 if (page == null) { return null; } 153 154 try { 155 KeyGenerator keygen = KeyGenerator.getInstance("DESede"); 156 157 //Since we don't initialize the KeyGenerator, a system-provided source 158 // of randomness will be used to create the DES key 159 SecretKey desKey = keygen.generateKey(); 160 161 // Use triple DES 162 Cipher encrypter = Cipher.getInstance("DESede"); 163 encrypter.init(Cipher.ENCRYPT_MODE, desKey); 164 165 // Encrypt the bits and return the result 166 result = encrypter.doFinal(page); 167 } catch (IllegalBlockSizeException e) { 168 throw new RuntimeException("Page data length is invalid (must be divisible by 8): " 169 + page.length, e); 170 } catch (Exception e) { 171 System.err.println("Oops, we hit an encryption bug."); 172 e.printStackTrace(); 173 result = null; 174 //System.exit(3); 175 } 176 177 return result; 178 } 179 180 181 // XXX what is "if we hit problems" supposed to mean!? 182 // XXX the zero-padding is a temporary fix -- really, just the gzipped bytes should be returned 183 /** 184 * Returns the result of compressing the given page or null if we hit problems 185 * XXX Remove the temporary hack of zero-padding 186 * @param page An array of FILE_READ_SIZE bytes to be compressed 187 * @return The compressed data, with zeros appended to make the length 188 * FILE_READ_SIZE bytes 189 */ 190 public byte [] compressPage(byte[] page) 191 { 192 // Pipeline to do compression 193 GZIPOutputStream zipOut = null; 194 ByteArrayOutputStream byteOut = null; 195 196 if (page == null) { return null; } 197 198 // Initialize streams 199 try { 200 byteOut = new ByteArrayOutputStream(); 201 zipOut = new GZIPOutputStream(byteOut); // Write the compressed data to the byte stream 202 } catch (Exception e) { 203 System.err.println("Error initializing compression."); 204 e.printStackTrace(); 205 return null; 206 } 207 208 // Compress page 209 try { 210 zipOut.write(page, 0, page.length); // Write compressed page to byte buffer 211 zipOut.finish(); 212 213 byte[] tmp = byteOut.toByteArray(); // Retrieve compressed bytes 214 byte[] result = new byte[DATA_LENGTH]; 215 216 // Copy the first 4096 bytes into the result array. 217 // Because result was initialized as all zeros, any necessary 218 // zero-padding is already done (in the event that tmp is shorter 219 // than result). 220 System.arraycopy(tmp, 0, result, 0, Math.min(tmp.length, result.length)); 221 222 zipOut.close(); // Also closes the underlying stream (byteOut) 223 224 assert(result.length == DATA_LENGTH); 225 return result; 226 } catch (IOException e) { 227 throw new RuntimeException("Compression error", e); 228 // result = null; 229 } 230 } 231 232 233 234 235 /** 236 * Returns a page (FILE_READ_SIZE bytes) of random data. 237 */ 238 byte[] getRandomData() { 239 byte[] page; 240 241 // Clear out old results 242 Arrays.fill(rfData, (byte) 0); 243 244 /* Generate one page of random data */ 245 if (genFileData(rootFile) ) { 246 //System.out.println("Got data"); 247 248 page = new byte[rfData.length]; 249 250 // Transfer data 251 for (int i=0; i < rfData.length; i++) { 252 page[i] = rfData[i]; 253 } 254 } else { 255 System.err.println("Failed to get data"); 256 return null; 257 } 258 259 /* Debugging: 260 System.out.println("\n\nPage 1:"); 261 for (int i = 0; i < page1.length; i++) { 262 System.out.print(page1[i]); 263 if (i % 100 == 0) { 264 System.out.print("\n"); 265 } 266 } 267 */ 268 269 return page; 270 } 271 272 273 /** Function to gather random data from the file system 274 Randomly selects a target depth and then calls fileSearch 275 to do the actual descent */ 276 public boolean genFileData(File startNode) { 277 retrData = 0; 278 int targetDepth; 279 280 // Loop until we get enough data 281 do { 282 // Loop until we have a successful search 283 do { 284 targetDepth = myRand.nextInt(MAX_DEPTH); 285 } while (!fileSearch(startNode, targetDepth, 0)); 286 } while (retrData < DATA_LENGTH); 287 return true; 288 } 289 290 /** 291 a simple recursive function that recurses through 292 the file system and puts random data into the rfData array 293 */ 294 boolean fileSearch (File recurFile, int targetDepth, int curDepth) 295 { 296 File[] fileList; 297 File selectedFile; 298 299 if (curDepth == targetDepth) { // We've reached bottom. Pick a file 300 fileList = recurFile.listFiles(new FileFilter() { 301 public boolean accept(File name) { 302 return name.isFile(); 303 }}); 304 if (fileList == null || fileList.length == 0) { return false;} 305 306 selectedFile = fileList[myRand.nextInt(fileList.length)]; 307 try { 308 if (DEBUG) System.err.println("File read: " + selectedFile.getAbsolutePath()); 309 int read; 310 FileInputStream myFIS = new 311 FileInputStream(selectedFile); 312 read = myFIS.read(rfData, retrData, 313 DATA_LENGTH-retrData); 314 if (read == -1) { // Nothing left to read 315 return false; 316 } else { 317 retrData += read; 318 } 319 } catch (FileNotFoundException ex) { 320 return false; 321 } catch (IOException ex) { 322 // Don't print anything; we can't do anything about it, so 323 // the output is just confusing -- instead, silently fail and 324 // retry 325 326 //System.err.println("Error reading file:" + ex.getMessage()); 327 //ex.printStackTrace(); 328 return false; 329 } 330 331 return true; // Succeeded at reading this file 332 } else { // We need to keep going down 333 File[] dirList = recurFile.listFiles(new FileFilter() { 334 public boolean accept(File name) { 335 // XXX ignore directories named dev (to avoid /dev/* on Unix 336 // systems, because many devices (e.g. keyboard, tty, ...) will 337 // block when read 338 // FIXME come up with a better solution 339 return name.isDirectory() && !name.getName().equals("dev"); 340 }}); 341 342 if (dirList == null || dirList.length == 0) { 343 return false; // Can't go any lower 344 } else { 345 // Pick a random directory and recurse 346 return fileSearch(dirList[myRand.nextInt(dirList.length)], 347 targetDepth, curDepth+1); 348 } 349 } 350 } 351 }