How can I improve the processing speed to generate a unique code - php

I need to generate about 100000 unique code. I have tried following code. But it is getting slower. Can anybody suggest me how can I make it faster, soon I have to generate unique code of 1M.
$uniqueArray = array();
for($i = 1; $i <= 100000; $i++) {
$pass = substr(md5(uniqid(mt_rand(), true)) , 0, 6);
if (!in_array($pass, $uniqueArray)) {
$uniqueArray[] = 'AB' . $pass;
}
}

As is, your code is not guaranteed to generate a given amount of unique values. You would need to keep track of the actual number of values in $uniqueArray to be sure.
That said, you can speed up the generation by not searching in the array but by using the array as a map (key => value), e.g.:
<?php
declare(strict_types=1);
error_reporting(-1);
ini_set('display_errors', 'On');
/**
* #return string
*/
function createUniqueCode(): string
{
// ...or any other implementation
return 'AB' . substr(md5(uniqid((string)mt_rand(), true)), 0, 6);
}
$uniqueMap = [];
$n = 10000;
while ($n > 0) {
$pass = createUniqueCode();
if (!isset($uniqueMap[$pass])) {
$uniqueMap[$pass] = true;
$n--;
}
}
$codes = array_keys($uniqueMap);
echo count($codes);

<?php
$count = 100000;
$uniqueArray = range(0,$count-1);
shuffle($uniqueArray);
array_walk($uniqueArray,function(&$v,$k){$v = "AB".sprintf("%06d",$v);});
Runtime <100ms for 100000 values.
Note: If you need the full uniqueness of the values then use the pure uniqueid and dont reduce the results with substr() or other functions.

Related

PHP create unique value compared to values from object

I need to create a unique value for $total, to be different from all other values from received object. It should compare total with order_amount from object, and then if it is the same, it should increase its value by 0.00000001, and then check again through that object to see if it matches again with another order_amount. The end result should be a unique value, with minimal increase compared to the starting $total value. All values are set to have 8 decmal places.
I have tried with the following but it won't get me the result i need. What am i doing wrong?
function unique_amount($amount, $rate) {
$total = round($amount / $rate, 8);
$other_amounts = some object...;
foreach($other_amounts as $amount) {
if ($amount->order_amount == $total) {
$total = $total + 0.00000001;
}
}
return $total;
}
<?php
define('EPSILON',0.00000001);
$total = 4.00000000;
$other_amounts = [4.00000001,4.00000000,4.00000002];
sort($other_amounts);
foreach($other_amounts as $each_amount){
if($total === $each_amount){ // $total === $each_amount->order_amount , incase of objects
$total += EPSILON;
}
}
var_dump($total);
OUTPUT
float(4.00000003)
You may add an additional break if $total < $each_amount to make it a bit more efficient.
UPDATE
To sort objects in $other_amounts based on amount, you can use usort.
usort($other_amounts,function($o1,$o2){
if($o1->order_amount < $o2->order_amount ) return -1;
else if($o1->order_amount > $o2->order_amount ) return 1;
return 0;
});
Ok, here's the solution I came up with. First I created a function to deliver random objects with random totals so I could work with, unnecessary for you but useful for the sake of this test:
function generate_objects()
{
$outputObjects = [];
for ($i=0; $i < 100; $i++) {
$object = new \stdClass();
$mainValue = random_int(1,9);
$decimalValue = random_int(1,9);
$object->order_amount = "{$mainValue}.0000000{$decimalValue}";
$outputObjects[] = $object;
}
return $outputObjects;
}
And now for the solution part, first the code, then the explanation:
function unique_amount($amount, $rate) {
$total = number_format(round($amount / $rate, 8), 4);
$searchTotal = $total;
if (strpos((string) $searchTotal, '.') !== false) {
$searchTotal = str_replace('.', '\.', $searchTotal);
}
$other_amounts = generate_objects();
$similarTotals = [];
foreach($other_amounts as $amount) {
if (preg_match("/^$searchTotal/", $amount->order_amount)) {
$similarTotals[] = $amount->order_amount;
}
}
if (!empty($similarTotals)) {
rsort($similarTotals);
$total = ($similarTotals[0] + 0.00000001);
}
// DEBUG
//echo '<pre>';
//$vars = get_defined_vars();
//unset($vars['other_amounts']);
//print_r($vars);
//die;
return $total;
}
$test = unique_amount(8,1);
echo $test;
I decided to use RegEx to find the amounts that starts with the ones I provided. Since in the exercise I provided only integers with 1-9 in the last decimal case, I tracked them and added them to one array $similarTotals.
Then I sorted this array, if not empty, descending by the values, got the first item and incremented by 0.00000001.
So, finally, returning either the $total (assuming nothing was found) or the incremented first item in the array.
PS. I did not expect this code to be this big but, well...
You can see the test here: https://3v4l.org/WGThI

How to make uniqid shorter

I'd like to use php uniqid() in a smarty template for my small reservation system (for a product) to generate an UNIQUE value that would make for a reservation number.
Default uniqid() is a bit too long for my purpose, how can I make it like 5-6 characters?
tentative answer:
<?php
function toBase(/* positiv integer*/ $n, array $alphabet) {
$retval = '';
do {
$retval .= $alphabet[ $n%count($alphabet) ];
$n = intval( $n / count($alphabet) );
}
while( ($n=intval($n)) > 0);
return $retval;
}
function getCode() {
static $alphabet = null;
if( $alphabet==null) {
$alphabet = str_split('3479ACEFHJKLMNPRTUVWXY');
}
// get a random number
// and "encode" it using the alphabet
$code = toBase(mt_rand(), $alphabet);
// this might be both
// - too long
// - and too short*
// so first get the last 6 characters (if there are that much)
$code = substr($code, -6);
// and if there wasn't, pad them with 'zeros' (according to the alphabet that's a '3')
$code = str_pad($code, 6, $alphabet[0]);
return $code;
// *) the "too short" part could be avoided via mt_rand(22^6, ...)
// but I want to keep it in the range of a 32bit signed integer
}
getCode() gives you codes like
YFTRXA
MRMTMV
YC9HVN
VWCAUE
JEVXUF
WWMEYU
KLWAML
YCKE3V
37KJ3P
ME9EKU
I've tested getCode() (once) via
function testCodes() {
$codes = [];
for($i=0; $i<2000; $i++) {
$codes[] = getCode();
}
$withoutCollisions = array_unique($codes);
return count($codes)-count($withoutCollisions);
}
$collisions = [];
for($i=0; $i<5000; $i++) {
$c = testCodes();
if ( !isset($collisions[$c]) ) {
$collisions[$c] = 0;
}
$collisions[$c] += 1;
}
var_dump($collisions);
and the output was
array(3) {
[0]=>
int(4899)
[1]=>
int(100)
[2]=>
int(1)
}
So there are collisions (a set of 2000 codes having one or two doublets) but I'd say for what you're supposedly trying to achieve it's in the ball park. Collision rate is low enough so that you could even place a unique contraint in the database on that field and simply try again on a collison.
....BUT feel free to get over to https://security.stackexchange.com/ and have this algorithm shred to pieces ;-)

Embedding data in a serial number

I am looking at implementing a PVKS as outlined here. I have it working as presented in the article (but in PHP), however I have an extra requirement I'm trying to figure out how to fulfill. I'm trying to figure out how I should embed a date and 3 digit number into the generated code. I'm not even sure where to start here, so I honestly haven't tried anything. They unfortunately won't be unique in combination, so I can't hash them for the seed value. As far as I can tell I can't have them be part of the getKeyByte function either, as the inputs to that function should be only the seed and some arguments, where those arguments define the algorithm for a valid key. They have to be the same between the generator and validator so they have to be static. Is there an accepted practice for this kind of task?
Managed to figure this one out myself. I realized I was over complicating things by trying to find a way I could encode a value into a keybyte. The way to do this (well, the way I did this) is just to add extra bytes after the keybytes and before the checksum. They don't need to be validated like the other keybytes, but do affect the checksum.
Below is the part of my class related to generating and validating keys, with the new sections I needed to add noted.
static public function generateKey($extraKeyArgs = array(), $encodedData = array())
{
$args = self::instanceKeyArgs($extraKeyArgs);
$keyBytes = array();
$seed = self::getSeed();
$hexSeed = self::intToHex($seed,self::$seedWidth);
$key = $hexSeed;
$numKeys = count($args);
for ($i=0; $i < $numKeys; $i++) {
list($a, $b, $c) = $args[$i];
$keyBytes[$i] = self::getKeyByte($seed, $a, $b, $c, self::$keyWidthBytes);
$key .= self::intToHex($keyBytes[$i],self::$keyWidthHex);
}
// Section added to handle encoded data
foreach ($encodedData as $data) {
// Make $data an integer value, one byte wide.
$data = (((int) $data) & 255);
$keyBytes[] = $data;
$numKeys++;
$key .= self::intToHex($data,self::$keyWidthHex);
}
// End Section
$checksum = self::getChecksum($key);
$key = $hexSeed . self::$seperator;
for ($i=0; $i < $numKeys; $i++) {
$key .= self::intToHex($keyBytes[$i],self::$keyWidthHex);
if ($i & 1) {
$key .= self::$seperator;
}
}
if (substr($key, -1) !== self::$seperator) {
$key .= self::$seperator;
}
$key .= $checksum;
return $key;
}
static public function checkKey($key, $extraKeyArgs = array(), &$data = array())
{
$args = self::instanceKeyArgs($extraKeyArgs);
$numKeys = count($args);
if (!self::checkKeyChecksum($key)) {
return false; // Failed checksum! Maybe a mistype or optical reader error?
}
$key = self::normalizeKey($key);
// TODO - we would check against a blacklist here if we wanted to implement that.
$seed = hexdec(substr($key,0,self::$seedWidth));
if (!is_int($seed) || $seed < 1) {
return false; // Failed to get seed. Are you sure this key came from here?
}
$key = substr($key, self::$seedWidth, (strlen($key) - (self::$seedWidth + self::$checksumWidth)));
for ($i=0; $i < $numKeys; $i++) {
$keyByte = substr($key, 0, self::$keyWidthHex);
$key = substr($key, self::$keyWidthHex);
list($a, $b, $c) = $args[$i];
if ($keyByte !== self::intToHex(self::getKeyByte($seed, $a, $b, $c, self::$keyWidthBytes),2)) {
return false; // Key byte failed check. Possible forgery attempt?
}
}
// This line added to handle encoded data
$data = array_map('hexdec', str_split($key, self::$keyWidthHex));
return true; // Valid Key, Yay!
}

Check if a generated license is valid

I have a PHP script that generates some strings which will be used as license keys:
function KeyGen(){
$key = md5(microtime());
$new_key = '';
for($i=1; $i <= 25; $i ++ ){
$new_key .= $key[$i];
if ( $i%5==0 && $i != 25) $new_key.='-';
}
return strtoupper($new_key);
}
$x = 0;
while($x <= 10) {
echo KeyGen();
echo "<br />";
$x++;
}
After running the script once, I got these:
8B041-EC7D2-0B9E3-09846-E8C71
C8D82-514B9-068BC-8BF80-05061
A18A3-E05E5-7DED7-D09ED-298C4
FB1EC-C9844-B9B20-ADE2F-0858F
E9AED-945C8-4BAAA-6938D-713ED
4D284-C5A3B-734DF-09BD6-6A34C
EF534-3BAE4-860B5-D3260-1CEF8
D84DB-B8C72-5BDEE-1B4FE-24E90
93AF2-80813-CD66E-E7A5E-BF0AE
C3397-93AA3-6239C-28D9F-7A582
D83B8-697C6-58CD1-56F1F-58180
What I now am trying to do is change it so that I have another function that will check if the key has been generated using my script. Currently, what I am thinking is setting the $key to the MD5 of one specific string (for example, test) but, of course, that returns all the strings the same.
Can anyone help?
There are three basic ways of handling this. How you do it will depend on how many keys you're generating, and how important is may be to be able to invalidate keys at a later day. Which you choose is up to you.
Option 1: Server Database Storage
When the server generates a key (like using your algorithm), you store it in a database. Then later all you need to do to check the key is see if it's in the database.
Note that your algorithm needs a lot more entropy than you're providing it. The current timestamp is NOT enough. Instead, use strong randomness:
$key = mcrypt_create_iv($length_needed, MCRYPT_DEV_URANDOM);
Or, if you don't have mcrypt:
$key = openssl_random_pseudo_bytes($length_needed);
Or if you don't have mcrypt and openssl, use a library
Note that md5 returns a hex output (a-f0-9), where all of the above return full random binary strings (characters 0 - 255). So either base64_encode() it, or bin2hex() it.
Pros:
Simple to implement
Can "deactive" issued keys at a later date
Impossible to forge a new key
Cons:
Requires persistent storage per key
May not scale that well
Requires "key server" to validate keys
Option 2: Signing Keys
Basically, you generate a strong random key (from here out called the private key), and store it on your server. Then, when generating the license key, you generate a random blob, and then HMAC sign it with the private key, and make the license part of that block. That way, you don't need to store each individual key.
function create_key($private_key) {
$rand = mcrypt_create_iv(10, MCRYPT_DEV_URANDOM);
$signature = substr(hash_hmac('sha256', $rand, $private_key, true), 0, 10);
$license = base64_encode($rand . $signature);
return $license;
}
function check_key($license, $private_key) {
$tmp = base64_decode($license);
$rand = substr($tmp, 0, 10);
$signature = substr($tmp, 10);
$test = substr(hash_hmac('sha256', $rand, $private_key, true), 0, 10);
return $test === $signature;
}
Pros:
Simple to implement
Does not require persistent storage
Trivial to scale
Cons:
Cannot "Deactivate" keys individual
Requires storing "private keys"
Requires "key server" to validate keys.
Option 3: Public Key Crypto
Basically, you generate a public/private key pair. You embed the public key in your application. Then, you generate a key (similar to "signing keys" above), but instead of signing it with the HMAC signature, you sign it with a private key.
That way, the application (which has the public key) can verify the signature directly without needing to call back to your server.
function create_key($private_key) {
$rand = mcrypt_create_iv(10, MCRYPT_DEV_URANDOM);
$pkeyid = openssl_get_privatekey($private_key);
openssl_sign($rand, $signature, $pkeyid);
openssl_free_key($pkeyid);
$license = base64_encode($rand . $signature);
return $license;
}
function check_key($license, $public_key) {
$tmp = base64_decode($license);
$rand = substr($tmp, 0, 10);
$signature = substr($tmp, 10);
$pubkeyid = openssl_get_publickey($public_key);
$ok = openssl_verify($rand, $signature, $pubkeyid);
openssl_free_key($pubkeyid);
return $ok === 1;
}
Pros:
Simple to implement
Does not require persistent storage
Trivial to scale
Does not require "key server" to validate keys
Cons:
Cannot "Deactivate" keys individual
Requires storing "private keys"
Note:
This solution is on the assumption you want your licence key to always be in fixed format (see below) and still self authenticated
FORMAT : XXXXX-XXXXX-XXXXX-XXXXX-XXXX
If that is not the case refer to #ircmaxell for a better solution
Introduction
Self authenticated serial is tricky solution because:
Limited Size of Serial
It need to authenticate it self without Database or any storage
If private key is leaked .. it can easily be reversed
Example
$option = new CheckProfile();
$option->name = "My Application"; // Application Name
$option->version = 0.9; // Application Version
$option->username = "Benedict Lewis"; // you can limit the key to per user
$option->uniqid = null; // add if any
$checksum = new Checksum($option);
$key = $checksum->generate();
var_dump($key, $checksum->check($key));
Output
string '40B93-C7FD6-AB5E6-364E2-3B96F' (length=29)
boolean true
Please note that any modification in the Options would change the key and make it invalid;
Checking for collision
I just ran this simple test
set_time_limit(0);
$checksum = new Checksum($option);
$cache = array();
$collision = $error = 0;
for($i = 0; $i < 100000; $i ++) {
$key = $checksum->generate();
isset($cache[$key]) and $collision ++;
$checksum->check($key) or $error ++;
$cache[$key] = true;
}
printf("Fond %d collision , %d Errors in 100000 entries", $collision, $error);
Output
Fond 0 collision , 0 Errors in 100000 entries
Better Security
By default the script uses sha1 but PHP has a lot of better hash functions you can get that with the following code
print_r(hash_algos());
Example
$checksum = new Checksum($option, null, "sha512");
Class Used
class Checksum {
// Used used binaray in Hex format
private $privateKey = "ec340029d65c7125783d8a8b27b77c8a0fcdc6ff23cf04b576063fd9d1273257"; // default
private $keySize = 32;
private $profile;
private $hash = "sha1";
function __construct($option, $key = null, $hash = "sha1") {
$this->profile = $option;
$this->hash = $hash;
// Use Default Binary Key or generate yours
$this->privateKey = ($key === null) ? pack('H*', $this->privateKey) : $key;
$this->keySize = strlen($this->privateKey);
}
private function randString($length) {
$r = 0;
switch (true) {
case function_exists("openssl_random_pseudo_bytes") :
$r = bin2hex(openssl_random_pseudo_bytes($length));
break;
case function_exists("mcrypt_create_ivc") :
default :
$r = bin2hex(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM));
break;
}
return strtoupper(substr($r, 0, $length));
}
public function generate($keys = false) {
// 10 ramdom char
$keys = $keys ? : $this->randString(10);
$keys = strrev($keys); // reverse string
// Add keys to options
$this->profile->keys = $keys;
// Serialise to convert to string
$data = json_encode($this->profile);
// Simple Random Chr authentication
$hash = hash_hmac($this->hash, $data, $this->privateKey);
$hash = str_split($hash);
$step = floor(count($hash) / 15);
$i = 0;
$key = array();
foreach ( array_chunk(str_split($keys), 2) as $v ) {
$i = $step + $i;
$key[] = sprintf("%s%s%s%s%s", $hash[$i ++], $v[1], $hash[$i ++], $v[0], $hash[$i ++]);
$i ++; // increment position
}
return strtoupper(implode("-", $key));
}
public function check($key) {
$key = trim($key);
if (strlen($key) != 29) {
return false;
}
// Exatact ramdom keys
$keys = implode(array_map(function ($v) {
return $v[3] . $v[1];
}, array_map("str_split", explode("-", $key))));
$keys = strrev($keys); // very important
return $key === $this->generate($keys);
}
}
What you are actually looking for is an algorithm like Partial Key Validation
See this article for the workings and port it to PHP
http://www.brandonstaggs.com/2007/07/26/implementing-a-partial-serial-number-verification-system-in-delphi/
Store these keys in a database when you create them.Later match them with the database rows and voila..It will be done
Note that it's not impossible that you will get duplicate keys with this algorithm, it's unlikely, but so is winning the lottery. You will have to store the keys in a database or file to check if it allready exists.

PHP random set of images

I'm using a php script to randomly show images. I've duplicated this script three times because I wanted to show three random images at once - I'm unsure of how to change the php code to show 3 images.
The problem is, I don't want to run into the chance of all three scripts showing the same images at once. Is there something that I could add to this code to make sure that each image displayed is always different?
<?php
$random = "random.txt";
$fp = file($random);
srand((double)microtime()*1000000);
$rl = $fp[array_rand($fp)];
echo $rl;
?>
the html:
<?php include("rotate.php"); ?>
<?php include("rotate.php"); ?>
<?php include("rotate.php"); ?>
*the random.txt just has a list of filenames with links.
Simple solution...
Get the array of random images (you already did this)
Shuffle the array
Pop an image off the end of the array whenever you need one
rotate.php
$random = "random.txt";
$fp = file($random);
shuffle($fb); //randomize the images
in your code
<?php include('rotate.php') ?>
Whenever you need an image
<?php echo array_pop( $fb ) ?>
http://php.net/manual/en/function.array-pop.php
function GetRandomItems($arr, $count)
{
$result = array();
$rcount = 0;
$arrsize = sizeof($arr);
for ($i = 0; ($i < $count) && ($i < $arrsize); $i++) {
$idx = mt_rand($rcount, $arrsize);
$result[$rcount] = trim($arr[$idx]);
$arr[$idx] = $arr[$rcount];
$rcount++;
}
return $result;
}
$listname = "random.txt";
$list = file($listname);
$random = GetRandomItems($list, 3);
echo implode("<BR>", $list);
P.S. Actually, Galen's answer is better. For some reason I forgot about shuffle xD
You can use array_rand() to select more than one random key at a time, like this:
$random = "random.txt";
$fp = file($random);
shuffle($fp);
// You don't need this. The array_rand() function
// is automatically seeded as of 4.2.0
// srand((double)microtime()*1000000);
$keys = array_rand($fp, 3);
for ($i = 0; $i < 3; $i++):
$rl = $fp[$keys[$i]];
echo $rl;
endfor;
This would eliminate the need for including the file multiple times. It can all be done at once.
You could write a recursive function to check if the array ID has already been printed, and if it has, call itself again. Just put that in a for loop to print three times :)
Though keep in mind that truly random images could overlap!
$beenDisplayed = array();
function dispRand($id) {
if (in_array($id, $beenDisplayed)) {
//generate random number
dispRand($id);
}
else {
array_push($beenDisplayed, $id);
}
}
for ($i = 0; $i < 3; $i++) {
dispRand($random_id);
}

Categories