/** * LaboonHash - A simple cryptographic hash function using * Merkle-Damgard transforms and Merkle-Damgard strengthening. * Blocks are eight bytes and output is two bytes (16 bit). * Final blocks are strengthened (padded), if necessary, by length * of original string, modulo (10 ^ num_characters_to_pad). * Initialization value = AACC */ import java.lang.Math; import java.util.Arrays; public class LaboonHash { // Initialization Value public static final byte[] INITIAL_VALUE = { (byte) 0xAA, (byte) 0xCC }; // Size of blocks (in bytes) public static final int BLOCK_SIZE = 8; // Size of results of compressin function (in bytes) public static final int RESULT_SIZE = 2; /** * Given some arbitrary byte array bytes, convert it to a hex string. * Example: [0xFF, 0xA0, 0x01] -> "FFA001" * @param bytes arbitrary-length array of bytes * @return String hex string version of byte array */ public static String convertBytesToHexString(byte[] bytes) { StringBuffer toReturn = new StringBuffer(); for (int j = 0; j < bytes.length; j++) { String hexit = String.format("%02x", bytes[j]); toReturn.append(hexit); } return toReturn.toString(); } /** * Given a string, calculate how many blocks of size BLOCK_SIZE * it should be split into. * @param s String to checl * @return int number of blocks */ public static int calculateNumBlocks(String s) { int numBlocks = -1; int len = s.length(); if (len % BLOCK_SIZE == 0) { numBlocks = len / BLOCK_SIZE; } else { numBlocks = (len / BLOCK_SIZE) + 1; } return numBlocks; } /** * Generate initial (empty) byte blocks given some string * @param s String to generate blocks from * @return byte[][] empty blocks (will be filled up later) */ public static byte[][] generateInitialBlocks(String s) { int numBlocks = calculateNumBlocks(s); byte[][] toReturn = new byte[numBlocks][1]; return toReturn; } /** * Given a String s and an original length, will fill up the remaining * space in a block with the original length of the string converted * to standard ASCII numerals modulo the size of the remaining space. * For example, given an original string 1234567890A (length = 11), * and a block size of eight, the blocks will start as * ["12345678", "90A"]. We want to pad the final block "09A" so that * it is eight chars/bytes long. So we take 11 % 10^5, i.e., * 11 % 10000, i.e., 11, and zero-pad that value. So the padded block * should be "90A00011" * Assume a new string 1234567890ABCDE, length = 15, same block size * of 8, so original string blocks = ["12345678", "90ABCDE"]. Now * we take 15 % 10 ^ 1, i.e. 15 % 10, i.e. 5, and add that to the * end of the block, so the final padded block is "90ABCDE5". * This is known as Merkle-Damgard strengthening or padding. * NOTE: Padding can have other meanings in cryptography. * @param s string block to pad * @param len length of original string * @return String padded version of block */ public static String pad(String s, int len) { int sizeToPad = BLOCK_SIZE - s.length(); int modValue = (int) Math.pow(10, sizeToPad); int moddedLen = len % modValue; String padded = String.format("%0" + sizeToPad + "d", moddedLen); return padded; } /** * Pad the final block of the array if necessary. * Necessary when the final block size is less than BLOCK_SIZE. * All other blocks besides the last one are guaranteed to be BLOCK_SIZE * bytes long, so this is the only one to check. * @see pad() method for algorithm * @param String[] initial blocks * @return String[] padded (if necessary) */ public static String[] strengthenIfNecessary(int origLength, String[] stringBlocks) { // Theoretically, we could not pass in origLength, and // re-calculate length of original string by adding up all // of the strings in stringBlocks. This seemed more straightforward // despite the extra argument, however. String finalBlock = stringBlocks[stringBlocks.length - 1]; int finalBlockLength = finalBlock.length(); if (finalBlockLength < BLOCK_SIZE) { String paddedBlock = finalBlock + pad(finalBlock, origLength); stringBlocks[stringBlocks.length - 1] = paddedBlock; } return stringBlocks; } /** * Given a String s, split into blocks of at most size BLOCK_SIZE. * The following examples assume a BLOCK_SIZE of 8. * Example: "AAA" -> ["AAA"] * Example: "AAAAAAAABBBBBBBB" -> ["AAAAAAAA", "BBBBBBBB"] * Example: "FOOFOOFOO" -> ["FOOFOOFO", "O"] * Example: "FOOFOOBARBARQUUXQUUX" -> ["FOOFOOBA", "RBARQUUX", "QUUX"] * @param s String to split * @return String[] split string */ public static String[] splitBlocks(String s) { return s.split("(?<=\\G.{" + BLOCK_SIZE + "})"); } /** * Given an array of String blocks, convert them into byte blocks */ public static byte[][] stringBlocksToByteBlocks(String[] stringBlocks) { byte[][] toReturn = new byte[stringBlocks.length][BLOCK_SIZE]; for (int j = 0; j < stringBlocks.length; j++) { // System.out.println("(" + j + ") Converting " + stringBlocks[j]); byte[] bytes = stringBlocks[j].getBytes(); for (int k = 0; k < BLOCK_SIZE; k++) { // System.out.println("\t(" + k + ") Converting " + bytes[k]); toReturn[j][k] = bytes[k]; } } return toReturn; } /** * Given some string s, * 1. Split into blocks of size BLOCK_SIZE * 2. Merkle-Damgard strengthen (i.e., pad w/ len of string) final block * if necessary * 3. Convert String blocks (in String[]) to byte blocks (byte[][]) * There is also additional debug info which can be commented/uncommented to * follow along in the process. * @param s String to convert * @return byte[][] s converted to byte blocks */ public static byte[][] stringToByteBlocks(String s) { int origLength = s.length(); String[] stringBlocks = splitBlocks(s); stringBlocks = strengthenIfNecessary(origLength, stringBlocks); // Display string blocks System.out.println("String Blocks:"); for (String block : stringBlocks) { System.out.println(block); } byte[][] toReturn = stringBlocksToByteBlocks(stringBlocks); // Display byte blocks System.out.println("Byte Blocks:"); for (byte[] b : toReturn) { System.out.println(convertBytesToHexString(b)); } return toReturn; } /** * Given two byte arrays a1 and a2, concatenate together so that * they form a new array of length a1.length + a2.length with * all of the values in a1 in the front of the array and a2 in the * back. * Example: a1 = [0, 1, 2] * a2 = [3, 4] * Returns new array [0, 1, 2, 3, 4] * @param a1 first array to concatenate * @param a2 second array to concatenate * @return byte[] concatenated array */ public static byte[] concatArrays(byte[] a1, byte[] a2) { int len1 = a1.length; int len2 = a2.length; int c = 0; byte[] toReturn = new byte[a1.length + a2.length]; for (int j = 0; j < len1; j++) { toReturn[c++] = a1[j]; } for (int j = 0; j < len2; j++) { toReturn[c++] = a2[j]; } return toReturn; } /** * Compression function (labeled c in diagrams in book) * Given two byte arrays - a previous result and a block - * apply the following function: * 1. Concatenate result and block into a single byte array * 2. Generate a new two-byte (16-bit) result block w/ initial values * 3. For each byte b in the concatenated array: * a. result[0] = (prevResult1 xor b) + b * b. result[1] = (prevResult0 xor b) - b * 4. Return final result array * @param oldResult - the result being passed into this compression function * @param block - the block being passed into this compression function * @return byte[] - new result */ public static byte[] compress(byte[] oldResult, byte[] block) { byte[] combined = concatArrays(oldResult, block); byte[] result = { 65, 63 }; for (byte b : combined) { // Have to store this since it will be modified by the // line right after it byte origResult0 = result[0]; result[0] = (byte) ((byte) (result[1] ^ b) + (byte) b); result[1] = (byte) ((byte) (origResult0 ^ b) - (byte) b); } return result; } /** * Run entire LaboonHash compression function on all blocks. * This enumerates through all blocks and is thus valid for * any size input - see the section in the textbook on Merkle-Damgard * transforms for more information. * @param byteBlocks - bytes to run compression function on * @return byte[] - cryptographic hash of bytes */ public static byte[] compressAll(byte[][] byteBlocks) { byte[] result = INITIAL_VALUE; int numRounds = 0; for (byte[] b : byteBlocks) { System.out.print("Round " + numRounds++ + ": prev res = " + convertBytesToHexString(result) + ", block = " + convertBytesToHexString(b) + " --> "); result = compress(result, b); System.out.println(convertBytesToHexString(result)); } return result; } /** * Given a string, return its LaboonHash in bytes. * @param toHash - string to hash * @return byte[] - LaboonHash digest of string */ public static byte[] laboonHash(String toHash) { byte[][] blocks = stringToByteBlocks(toHash); byte[] toReturn = compressAll(blocks); return toReturn; } /** * Print usage information and exit program with exit code 1. */ public static void printUsageAndExit() { System.err.println("Enter a single string to hash"); System.exit(1); } public static void main(String[] args) { if (args.length != 1) { printUsageAndExit(); } byte[] result = laboonHash(args[0]); System.out.println("Hash: " + convertBytesToHexString(result)); } }