All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.main.java.com.github.toolarium.security.hash.HashId Maven / Gradle / Ivy

There is a newer version: 1.1.3
Show newest version
/*
 * HashId.java
 *
 * Copyright by toolarium, all rights reserved.
 */
package com.github.toolarium.security.hash;

import com.github.toolarium.common.util.StringUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;


/**
 * HashId: A small Java class to generate YouTube-like hashes from one or many numbers, ported from javascript hashids.js by Ivan Akimov.
 * It was designed for web sites to use in URL shortening, tracking stuff, or making pages private (or at least unguessable).
 * This algorithm tries to satisfy the following requirements: Hashes must be unique and decryptable.
 * They should be able to contain more than one integer (so you can use them in complex or clustered systems).
 * You should be able to specify minimum hash length.
 * Hashes should not contain basic English curse words (since they are meant to appear in public places - like the URL).
 * Instead of showing items as 1, 2, or 3, you could show them as U6dc, u87U, and HMou. You don't have to store these hashes 
 * in the database, but can encrypt + decrypt on the fly.
 * All (long) integers need to be greater than or equal to zero.
 * 
 * @author patrick
 */
public final class HashId {
    private static final String DEFAULT_ALPHABET = "xcS4F6h89aUbideAI7tkynuopqrXCgTE5GBKHLMjfRsz";

    private static final int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43 };
    private static final int[] SEPS_INDICES = {0, 4, 8, 12 };
    private String salt = "";
    private String alphabet = "";
    private int minHashLength;
    private List seps;
    private List guards;


    /**
     * Constructor for HashId
     * 
     * @param salt the salt
     * @param minHashLength the min length
     * @param alphabet the alphabet
     * @throws IllegalArgumentException In case of an invalid input
     */
    private HashId(String salt, int minHashLength, String alphabet) {
        if (alphabet == null || alphabet.isBlank()) {
            throw new IllegalArgumentException("alphabet must not be empty");
        }
        
        if (salt != null) {
            this.salt = salt;
        }
        
        if (minHashLength > 0) {
            this.minHashLength = minHashLength;
        }

        //this.alphabet = join(new LinkedHashSet(StringUtil.getInstance().splitAsList(alphabet, "")), "");
        List list = StringUtil.getInstance().splitAsList(alphabet, "");
        list = list.stream().distinct().collect(Collectors.toList());
        this.alphabet = join(list, "");
        if (this.alphabet.length() < 4) {
            throw new IllegalArgumentException("Alphabet must contain at least 4 unique characters.");
        }
        
        seps = new ArrayList();
        guards = new ArrayList();
        for (int prime : PRIMES) {
            if (prime < this.alphabet.length()) {
                char c = this.alphabet.charAt(prime - 1);
                seps.add(c);
                this.alphabet = this.alphabet.replace(c, ' ');
            }
        }

        for (int index : SEPS_INDICES) {
            if (index < seps.size()) {
                guards.add(seps.get(index));
                seps.remove(index);
            }
        }

        this.alphabet = consistentShuffle(this.alphabet.replaceAll(" ", ""), this.salt);
    }
    
    
    /**
     * Create a hash id class
     * 
     * @return the instance
     */
    public static HashId createHashId() {
        return new HashId("", 0, DEFAULT_ALPHABET);
    }

    
    /**
     * Create a hash id class
     * 
     * @param salt the salt
     * @return the instance
     */
    public static HashId createHashId(String salt) {
        return new HashId(salt, 0, DEFAULT_ALPHABET);
    }
    
    
    /**
     * Create a hash id class
     * 
     * @param salt the salt
     * @param minHashLength the min hash length
     * @return the instance
     */
    public static HashId createHashId(String salt, int minHashLength) {
        return new HashId(salt, minHashLength, DEFAULT_ALPHABET);
    }

    
    /**
     * Create a hash id class
     * 
     * @param salt the salt
     * @param minHashLength the min hash length
     * @param alphabet the alphabet
     * @return the instance
     */
    public static HashId createHashId(String salt, int minHashLength, String alphabet) {
        return new HashId(salt, minHashLength, alphabet);
    }

    
    /**
     * Get the salt
     * 
     * @return the salt
     */
    public String getSalt() {
        return salt;
    }

    
    /**
     * Get the alphabet
     * 
     * @return the alphabet
     */
    public String getAlphabet() {
        return alphabet;
    }

    
    /**
     * Get the min hash length
     * 
     * @return the min hash length
     */
    public int getMinHashLength() {
        return minHashLength;
    }

    
    /**
     * Encrypt
     * 
     * @param inputNumbers some numbers
     * @return the generated hash id
     */
    public String encrypt(long... inputNumbers) {
        long[] numbers = new long[inputNumbers.length];
        for (int i = 0; i < numbers.length; i++) {
            if (inputNumbers[i] < 0) {
                numbers[i] = -1 * inputNumbers[i];
            } else {
                numbers[i] = inputNumbers[i];
            }
        }

        return encode(numbers, alphabet, salt, minHashLength);
    }

    
    /**
     * Decrypt
     * 
     * @param hash the hash to decrypt
     * @return the decrypted hash
     */
    public long[] decrypt(String hash) {
        long[] inputNumbers = decode(hash);
        long[] numbers = new long[inputNumbers.length];

        for (int i = 0; i < inputNumbers.length; i++) {
            if (inputNumbers[i] < 0) {
                numbers[i] = -1 * inputNumbers[i];
            } else {
                numbers[i] = inputNumbers[i];
            }
        }

        return numbers;
    }

    
    /**
     * Encode
     * 
     * @param numbers the numbers
     * @param inputAlphabet the alphabet
     * @param inputSalt the salt
     * @param inputMinHashLength the min length
     * @return the hash
     */
    private String encode(long[] numbers, String inputAlphabet, String inputSalt, int inputMinHashLength) {
        String alphabet = inputAlphabet;
        String ret = "";
        String shuffeltSeps = consistentShuffle(join(seps, ""), join(numbers, ""));
        char lotteryChar = 0;

        for (int i = 0; i < numbers.length; i++) {
            if (i == 0) {
                String lotterySalt = join(numbers, "-");
                for (long number : numbers) {
                    lotterySalt += "-" + (number + 1) * 2;
                }
                
                String lottery = consistentShuffle(alphabet, lotterySalt);
                lotteryChar = lottery.charAt(0);
                ret += lotteryChar;

                alphabet = lotteryChar + alphabet.replaceAll(String.valueOf(lotteryChar), "");
            }

            alphabet = consistentShuffle(alphabet, (lotteryChar & 12345) + inputSalt);
            ret += hash(numbers[i], alphabet);

            if (i + 1 < numbers.length) {
                ret += shuffeltSeps.charAt((int) ((numbers[i] + i) % shuffeltSeps.length()));
            }
        }

        if (ret.length() < inputMinHashLength) {
            int firstIndex = 0;
            for (int i = 0; i < numbers.length; i++) {
                firstIndex += (i + 1) * numbers[i];
            }
            
            int guardIndex = firstIndex % guards.size();
            if (guardIndex >= 0) {
                char guard = guards.get(guardIndex);
                ret = guard + ret;
    
                if (ret.length() < inputMinHashLength) {
                    guardIndex = (guardIndex + ret.length()) % guards.size();
                    guard = guards.get(guardIndex);
                    ret += guard;
                }
            }
        }

        while (ret.length() < inputMinHashLength) {
            long[] padArray = new long[] {alphabet.charAt(1), alphabet.charAt(0) };
            String padLeft = encode(padArray, alphabet, inputSalt, 0);
            String padRight = encode(padArray, alphabet, join(padArray, ""), 0);

            ret = padLeft + ret + padRight;
            int excess = ret.length() - inputMinHashLength;
            if (excess > 0) {
                ret = ret.substring(excess / 2, excess / 2 + inputMinHashLength);
            }
            
            alphabet = consistentShuffle(alphabet, inputSalt + ret);
        }

        return ret;
    }
    
    
    /**
     * Decode
     * 
     * @param inputHash the input hash
     * @return the id's
     */
    private long[] decode(String inputHash) {
        String hash = inputHash;
        List ret = new ArrayList();
        String originalHash = hash;

        if (hash != null && !hash.isEmpty()) {
            String a = "";
            char lotteryChar = 0;

            for (char guard : guards) {
                hash = hash.replaceAll(String.valueOf(guard), " ");
            }
            
            String[] hashSplit = StringUtil.getInstance().splitAsArray(hash, " ");
            if (hashSplit.length == 3 || hashSplit.length == 2) {
                hash = hashSplit[1];
            } else {
                hash = hashSplit[0];
            }

            for (char sep : seps) {
                hash = hash.replaceAll(String.valueOf(sep), " ");
            }
            
            String[] hashArray = StringUtil.getInstance().splitAsArray(hash, " ");
            for (int i = 0; i < hashArray.length; i++) {
                String subHash = hashArray[i];

                if (subHash != null && !subHash.isEmpty()) {
                    if (i == 0) {
                        lotteryChar = hash.charAt(0);
                        subHash = subHash.substring(1);
                        a = lotteryChar + alphabet.replaceAll(String.valueOf(lotteryChar), "");
                    }
                }

                if (a.length() > 0) {
                    a = consistentShuffle(a, (lotteryChar & 12345) + salt);
                    ret.add(unhash(subHash, a));
                }
            }
        }

        long[] numbers = longListToPrimitiveArray(ret);
        if (!encrypt(numbers).equals(originalHash)) {
            return new long[0];
        }
        
        return numbers;
    }
    
    
    /**
     * Hash
     * 
     * @param inputNumber the input number
     * @param inputAlphabet the alphabet
     * @return the hash
     */
    private String hash(long inputNumber, String inputAlphabet) {
        long number = inputNumber;
        String hash = "";

        while (number > 0) {
            hash = inputAlphabet.charAt((int) (number % inputAlphabet.length())) + hash;
            number = number / inputAlphabet.length();
        }

        return hash;
    }

    
    /**
     * Unhash
     * 
     * @param hash the hash
     * @param inputAlphabet the alphabet
     * @return the result
     */
    private long unhash(String hash, String inputAlphabet) {
        long number = 0;

        for (int i = 0; i < hash.length(); i++) {
            int pos = inputAlphabet.indexOf(hash.charAt(i));
            number += pos * (long) Math.pow(inputAlphabet.length(), hash.length() - i - 1);
        }

        return number;
    }

    
    /**
     * Consistent shuffle
     * 
     * @param alphabet the alphabet 
     * @param inputSalt the input salt
     * @return the result
     */
    private static String consistentShuffle(String alphabet, String inputSalt) {
        String ret = "";
        String salt = inputSalt;

        if (!alphabet.isEmpty()) {
            if (salt == null || salt.isEmpty()) {
                salt = new String(new char[] {'\0' });
            }
            
            int[] sortingArray = new int[salt.length()];
            for (int i = 0; i < salt.length(); i++) {
                sortingArray[i] = salt.charAt(i);
            }

            for (int i = 0; i < sortingArray.length; i++) {
                boolean add = true;
                for (int k = i; k != sortingArray.length + i - 1; k++) {
                    int nextIndex = (k + 1) % sortingArray.length;

                    if (add) {
                        sortingArray[i] += sortingArray[nextIndex] + (k * i);
                    } else {
                        sortingArray[i] -= sortingArray[nextIndex];
                    }

                    add = !add;
                }

                sortingArray[i] = Math.abs(sortingArray[i]);
            }

            int i = 0;
            List alphabetArray = charArrayToStringList(alphabet.toCharArray());
            while (alphabetArray.size() > 0) {
                int pos = sortingArray[i];
                if (pos >= alphabetArray.size()) {
                    pos %= alphabetArray.size();
                }
                
                ret += alphabetArray.get(pos);
                alphabetArray.remove(pos);
                i = ++i % sortingArray.length;
            }
        }

        return ret;
    }
 
    
    /**
     * Convert long list into a primitive array 
     * 
     * @param longList the long list
     * @return the primitive array
     */
    private static long[] longListToPrimitiveArray(List longList) {
        long[] longArr = new long[longList.size()];
        int i = 0;

        for (long l : longList) {
            longArr[i++] = l;
        }

        return longArr;
    }

    
    /**
     * Convert a char array into a string list
     * 
     * @param chars the char array
     * @return the string list
     */
    private static List charArrayToStringList(char[] chars) {
        ArrayList list = new ArrayList(chars.length);
        for (char c : chars) {
            list.add(String.valueOf(c));
        }
        
        return list;
    }

    
    /**
     * Join long array with delimiter
     * 
     * @param longList the long value array
     * @param delimiter the delimiter
     * @return the string
     */
    private static String join(long[] longList, String delimiter) {
        ArrayList strList = new ArrayList(longList.length);
        for (long l : longList) {
            if (l < 0) {
                strList.add(String.valueOf(l));
            } else { 
                strList.add(String.valueOf(l));
            }
        }

        return join(strList, delimiter);
    }

    
    /**
     * Join collection with delimiter
     * 
     * @param c the collection
     * @param delimiter the delimiter
     * @return the string
     */
    private static String join(Collection c, String delimiter) {
        Iterator iter = c.iterator();
        if (iter.hasNext()) {
            StringBuilder builder = new StringBuilder(c.size());
            builder.append(iter.next());
            while (iter.hasNext()) {
                builder.append(delimiter);
                builder.append(iter.next());
            }

            return builder.toString();
        }

        return "";
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy