001// Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002// The Molly framework is freely distributable under the terms of an
003// MIT-style license. For details, see the molly pages web site at:
004// http://www.mollypages.org/. Use, modify, have fun !
005
006package fc.util;
007
008import java.util.*;
009import java.util.regex.*;
010import java.security.*;
011
012/* 
013UUID and token utilities
014
015@author hursh jain
016*/
017public final class UUIDUtil
018{
019static MessageDigest digester = null;
020
021static {
022  try {
023    digester = MessageDigest.getInstance("SHA-1");
024    }
025  catch (Exception e) {
026    e.printStackTrace();
027    }
028  }
029
030/**
031Returns a ID suitable for a session/cookie identifier.
032<pre>
033See: cookies.lcs.mit.edu
034See: www.across.si
035
036There are 2 issues with generating sessionid's. 
037
0381) uniqueness - 2 or more sessionid's should not end up being 
039   the same.
0402) hard-to-guess - For example, sequential values like 
041   1, 2, 3 are unique but easy to guess and therefore easy
042   to session hijack.
043
044Our sessionid's have 2 parts:
045a) a timestamp for guaranteed uniqueness  (easy to guess)
046b) random data (hard to guess)
047</pre>
048*/
049public static String newSessionID() 
050  {
051  //although this function has been written for some time,
052  //i recently looked this over with webscarab (around aug 2007)
053  //and it visually appears fairly scattered/random. (9999 sid's)
054  //dunno, maybe hotbits retrieved/cached every minute via a background
055  //thread is good if one wants to be hardcore but it's pretty good
056  //as-is 
057  
058  //there is also no visual difference between nanos & millis. intuitively
059  //nanos should have more differences in the lower bits but after
060  //all the bits get churned/digested, there is no diff that i can tell,
061  //at least visually via webscarab. so i've left this as millis.
062  
063  //final long time = System.currentTimeMillis();   //was seeing collisions
064  final long time = System.nanoTime(); 
065
066  Random random = new java.util.Random();
067  //final int rand = random.nextInt(); //was seeing collisions
068  final long rand = random.nextLong(); 
069  final String cookie = String.valueOf(time) + rand;
070
071  byte[] digestbuf = null;
072  
073  //digests always returns 16 bytes         
074  synchronized (UUIDUtil.class) {   
075    digestbuf = digester.digest(cookie.getBytes());
076      }
077    
078  /*
079  we simply convert bytes toHex. note, we don't use Base64
080  because '/' and '+' are valid Base64 chars and they can
081  mess up url's. 
082  */
083    
084    char[] encoded = null;
085    encoded = encode(digestbuf);
086    return new String(encoded);
087    
088    //note, we don't ever need to use the decode() method
089    //but it's there for testing foo->encode->decode->foo
090    }
091
092
093//well, i was very...young...when I wrote this... :-]
094//
095//all right, as of 2014, i took out the original string, a bit too risque
096//new lookup string is less embarassing
097//                1..............16 [or 0-15, 16 things]  
098//                0123456789ABCDEF
099static final char[] lookup="wInteEMubY2169x0".toCharArray();
100
101/*
102base16 encoding
103     0 -> some ascii code
104       1 -> some ascii code
105       ...
106       15-> some ascii code
107       
108when ascii codes are for 0...F it's called "hex" encoding. 
109*/
110
111//we need a reverse lookup table since are ain't
112//using straight hexadecimal to encode
113//encode: half byte [0-16] -> char value from lookup string
114//decode: char value from lookup string (0-128) -> half byte
115static final int[] reverselookup = new int[128];
116  
117static  
118  {
119  for (int n = 0; n < 16; n++) {
120    int x =  lookup[n] & 0xFF; //x ='c'('c'&0xff->99), 'n'(110) etc
121    reverselookup[x] = n;  //this is why reverselookup[128] above, not 16
122    }
123  }
124  
125private static final char[] encode(byte[] buf) 
126  {
127  final int len = buf.length;
128
129  char[] tmp = new char[len * 2];  
130  
131  for (int n = 0, i = 0; n < len ; n++, i+=2)
132    {
133    int high4 =  (buf[n] >> 4) & 0x0F;
134    int low4  =  buf[n] & 0x0F;
135    
136    tmp[i]   = lookup [high4];
137    tmp[i+1] = lookup [low4];
138    }
139  return tmp;
140  }
141
142private static final byte[] decode(char[] buf) 
143  {
144  final int len = buf.length;
145
146  byte[] tmp = new byte[(len/2) + (len%2)];
147
148  for (int n = 0, tmp_n = 0; n < len; n+=2, tmp_n++) 
149    {
150    //0xFF makes it an int. we could cast to (int)
151    //too but then that would sign extend the char
152    //in the general case [but since all are chars
153    //are < 127], it would be ok to do so in this
154    //case. Used 0xFF because that's the right thing
155    //generally speaking
156
157    int c1 = buf[n] & 0xFF;   //'w', 'I', 'n', 't' etc.. 
158    int c2 = buf[n+1] & 0xFF;
159
160    if ( c1 > 127 || c2 > 127)
161      throw new IllegalArgumentException("malformed bufing, encoded with characters I don't understand [" + c1 + "] and [" + c2 + "]");
162        
163    int i = (reverselookup[c1] << 4  | reverselookup [c2]); 
164    //System.out.println( (reverselookup[c1] << 4) + ", " + reverselookup[c2] + ",->" + i);
165
166    tmp[tmp_n] = (byte) i;
167    }
168  return tmp;
169  }
170  
171
172static char[] URL_safe_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789_-.~".toCharArray();
173
174/**
175Returns a N-char string suitable for tracking, affiliate, short codes, etc. Uses
176characters safe for all URL's.
177
178@param  len the length of the returned string
179*/
180public static String newID(int len)
181  {
182  final Random r = new Random();
183  final char[] buf = new char[len];
184  for (int n = 0; n < len; n++) {
185    buf[n] = URL_safe_chars[r.nextInt(URL_safe_chars.length)];
186    }
187  
188  return new String(buf);
189  }
190
191/**
192Returns a 8 character string suitable for tracking, affiliate, short codes, etc. Uses
193characters safe for all URL's.
194*/
195public static String newID()
196  {
197  return newID(8);
198  }
199    
200public static void main (String args[])
201  {
202  Args myargs = new Args(args);
203  int LOOP = myargs.getInt("loop", 100000);
204  myargs.setUsage("java fc.util.UUIDUtil [-loop <int, default 100,000>]");
205  
206  if (myargs.flagExists("help")) {
207    myargs.showError();
208    return;
209    }
210  System.out.println("Session ID: " + newSessionID());
211  System.out.println("Generating " + LOOP + " test session ids...");
212
213  fc.util.Watch w = new fc.util.Watch();
214  Map map = null;
215  int count = -1;
216  
217  w.start();  
218  for (int n = 0; n < LOOP; n++) {
219    String s = newSessionID();
220    }
221  System.out.println("Time for " + LOOP + ": " + w.time() + " ms");
222  
223  System.out.println("Now testing for collisions...");
224  w.restart();
225
226  map = new HashMap();
227  count = 0;
228  for (int n = 0; n < LOOP; n++) 
229    {
230    String s = newSessionID();
231    if (map.containsKey(s)) {
232      System.out.println("collision found with key: " + s);
233      count++;
234      }
235    if (n > 0 && n % 20000 == 0) {
236      w.stop();
237      System.out.println("generated: " + n + " (+ " + w.time() + " ms)");
238      w.restart();
239      }
240    map.put(s, null);
241    }
242  if (count == 0) {
243    System.out.println("No collisions found...");
244    }
245  else{
246    System.out.println(count + " collisions found...");
247    }
248
249  System.out.println("------------------------------");
250  w.restart();  
251  System.out.println("newID (8): " + newID(8));
252  System.out.println("Generating " + LOOP + " test session ids...");
253
254  for (int n = 0; n < LOOP; n++) {
255    String s = newID(8);
256    }
257  System.out.println("Time for " + LOOP + ": " + w.time() + " ms");
258
259  System.out.println("Now testing for collisions...");
260  w.restart();
261
262  map = new HashMap();
263  count = 0;
264  for (int n = 0; n < LOOP; n++) 
265    {
266    String s = newID(8);
267    if (map.containsKey(s)) {
268      System.out.println("collision found with key: " + s);
269      count++;
270      }
271    if (n > 0 && n % 20000 == 0) {
272      w.stop();
273      System.out.println("generated: " + n + " (+ " + w.time() + " ms)");
274      w.restart();
275      }
276    map.put(s, null);
277    }
278  if (count == 0) {
279    System.out.println("No collisions found...");
280    }
281  else{
282    System.out.println(count + " collisions found...");
283    }
284  }
285  
286
287}