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}