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    }