I was recently tasked with coming up with a system that would control user access to a variety of online products (e-books). The desired method was to have user accounts and "book codes". Each book code would be unique, and be associated with one of the publishers book and (once redeemed) a single user. Security wise, they needed to be assuredly unique but also non-sequential (and otherwise very hard to guess). As a usability requirement, I decided book codes should also be easily entered without resorting to copy-paste, contain only url-safe characters, and be as short as security allowed for.
Here's what I came up with (code is from a PHP class, using Codeigniter framework). It may do some extra work that's not strictly needed from a crypto standpoint, and implement some "security by obscurity" that's also not strictly needed but the basic question stands. Basic idea is that each code contains a base 64 encoding of its id separated by a non-base64 character from base 64 version of a sha1 hash that combines (among other things) that same ID. My presumption is the first part is assuredly unique (and remains so because of the separator) and the second part is (very) hard to guess, but (potentially) not unique. The code table has a 20 bit id field, so I figure at most I'd need 4 characters (plus the separator) to make each code unique, with a minimum (given my algorythms) of 2 plus the separator; the remaining characters are devoted just to making the code harder to guess. In this case, I'm using codes with 16 characters, providing 7-9 characters of "cryptography" (IE, a minumum of 42 bits) in each code.
Does this seem a reasonable and reliable solution? The codes in the db look good, but that's a rather small sample base...
/**
* creates unique new book codes and inserts them into user_book_code table
* code is based off book table id and current max value of user-book-codes id field
* returns true (inserts worked) or false (at least one failed insert or code creation attempt)
*
* @param int $id - book's id in books table
* @param in $num - number of codes to write
*/
public function make_book_codes($id,$num){
if ( (int)$id != $id) return false;
if ( (int)$num != $num) return false;
$ok = true;
$this->load->library('encrypt');
$query = $this->db->query('SELECT MAX(id) AS id_max FROM user_book_code');
$u= $query->row()->id_max;
for ( $i=0; $num >= $i ; $i++ ) { // changed to logical equivalent for posting reasons to be HTML safe, had less than symbol
if (!$ok) continue;
$codeA = $this->acid64hex( dechex($u++) );
$hash = $this->encrypt->sha1($u.$this->salt.$id);
$codeB = $this->acid64hex( $hash );
$code = strrev(substr( "$codeA+$codeB" , 0, $this->codeSize ));
$ok = $ok && $codeA && $codeB && $this->db->query(" INSERT INTO user_book_code (book_id,code) VALUES ($id, '$code') ") ;
}
if ($ok) {
return $this->get_book($id);
}
return false;
}
/**
* Returns a url-safe shortened version of a hex hash
* which "dissolves" the base 16 hex number into a base 64 encoding
* returned string will have 2 chars for every 3 (or fraction of) hex digits in hex hash
* returns false if hash is not hex value convertable string
*
* @param str $hash
*/
public function acid64hex($hash){
$return = '';
$hash = strtolower($hash);
$acid64 = '-qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890_';
$hashSmoke = array
('1','2','3','4','5','6','7','8','9','0','a','b','c','d','e','f');
$badHash = str_replace($hashSmoke,'',$hash);
if ($badHash){
return false;
}
$hashTrip = str_split($hash,3) ;
foreach ($hashTrip as $trip){
$pair10 = hexdec($trip);
$rA = $pair10%64;
$rB = floor($pair10/64);
$return .= $acid64[$rA] . $acid64[$rB];
}
return $return;
}
And yes, I did post this partly just because I had fun with function / variable names.