/******************************************************************************* * $Id: $ * Copyright (c) 2009-2010 Tim Tiemens. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v2.1 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * * Contributors: * Tim Tiemens - initial API and implementation ******************************************************************************/ package com.aegiswallet.helpers.secretshare.math; import com.aegiswallet.helpers.secretshare.SecretShareException; import com.aegiswallet.helpers.secretshare.hashchecks.Md5Checksummer; import com.aegiswallet.helpers.secretshare.hashchecks.Md5ChecksummerFactory; import java.math.BigInteger; /** * Data structure to support a "URI-like" string that allows BigInteger * values to be encoded as a hex string with a checksum. * <p/> * Syntax: * bigintcs:HHHHHH-HHHHHH-CCCCCC * Example: * bigintcs:bd2c52-b16d74-d51456-d0f89a-30c932-b2f6c1-3a9ce3-7b4387-0F2CA0 * <p/> * Note: negative BigIntegers are supported. This adds a "-" at the front, after the "bigintcs:" * e.g. -100 is the string 'bigintcs:-000064-BBC6EC' * The checksum takes the "-" into account, therefore, * 'bigintcs:-000064-BBC6EC' = -100 * 'bigintcs:000064-BBC6EC' = error * <p/> * The "HHHHHH-" section is repeated as many times as needed. * The first "HHHHHH-" section is 0-padded as needed. * The first section is either "HHHHHH" or "-HHHHHH" [dash means negative BigInteger]. * <p/> * The case of the digits "a-f" versus "A-F" does not matter. * Convention is that the HHHHHH- section is lower case, * the CCCCCC- section is upper case. */ public final class BigIntStringChecksum { // ================================================== // class static data // ================================================== /** * The Prefix string that identifies the 6hex-6hex-md5sum6hex pattern * we've invented here. */ public static final String PREFIX_BIGINT_DASH_CHECKSUM = "bigintcs:"; /** * Constant for how many characters/digits are in each "6hex" section. */ private static final int DIGITS_PER_GROUP = 6; // 16 as a constant private static final int HEX_RADIX = 16; // ================================================== // class static methods // ================================================== // ================================================== // instance data // ================================================== /** * Contains a hex-encoded [i.e. RADIX 16] string of the big integer. * Does not contain dashes ("-") */ private final String asHex; /** * Contains a hex-encoded string of the md5sum of "asHex". * Our implementation is limited to 6-characters. */ private final String md5checksum; // ================================================== // factories // ================================================== /** * Utility to test if the input string can even -possibly- be a BigIntStringChecksum encoded. * This is check is "necessary, but not sufficient" for the input string * to parse correctly. * * @param input the string to check * @return true if the string starts with * BigIntStringChecksum.PREFIX_BIGINT_DASH_CHECKSUM * false all other cases */ public static boolean startsWithPrefix(final String input) { boolean ret = false; if (input != null) { ret = input.startsWith(PREFIX_BIGINT_DASH_CHECKSUM); } else { ret = false; } return ret; } /** * Take input string, and create the instance. * * @param bics string in "bigintcs:HHHHHH-HHHHHH-CCCCCC" format * @return big integer string checksum object * @throws SecretShareException on error, such as null input, OR * input doesn't start with correct prefix OR * string does not have 0-9a-f digits OR * checksum doesn't match. */ public static BigIntStringChecksum fromString(String bics) { boolean returnIsNegative = false; BigIntStringChecksum ret = null; if (bics == null) { createThrow("Input cannot be null", bics); } if (startsWithPrefix(bics)) { String noprefix = bics.substring(PREFIX_BIGINT_DASH_CHECKSUM.length()); String noprefixnosign = noprefix; if (noprefixnosign.startsWith("-")) { returnIsNegative = true; noprefixnosign = noprefixnosign.substring(1); } String[] split = noprefixnosign.split("-"); if (split.length <= 1) { createThrow("Missing checksum section", bics); } else { String asHex = ""; if (returnIsNegative) { asHex = "-"; } for (int i = 0, n = split.length - 1; i < n; i++) { asHex += split[i]; } String computedMd5sum = computeMd5ChecksumLimit6(asHex); String givenMd5sum = split[split.length - 1]; if (computedMd5sum.equalsIgnoreCase(givenMd5sum)) { ret = new BigIntStringChecksum(asHex, computedMd5sum); } else { createThrow("Mismatch checksum given='" + givenMd5sum + "' computed='" + computedMd5sum + "'", bics); } } } else { createThrow("Input must start with '" + PREFIX_BIGINT_DASH_CHECKSUM + "'", bics); } // This should never throw an exception here. // But, if it does, better now than later: ret.asBigInteger(); return ret; } /** * Take string as input, and either return an instance or return null. * * @param bics string in "bigintcs:HHHHHH-HHHHHH-CCCCCC" format * @return big integer string checksum object * OR null if incorrect format, error parsing, etc. */ public static BigIntStringChecksum fromStringOrNull(String bics) { BigIntStringChecksum ret; if (bics == null) { ret = null; } else if (!startsWithPrefix(bics)) { ret = null; } else { try { ret = fromString(bics); // completely test the input: make sure // asBigInteger will throw a SecretShareException on error if (ret.asBigInteger() == null) { // asBigInteger() is not allowed to return null. // but just in case it does: throw new SecretShareException("Programmer error converting '" + bics + "' to BigInteger"); } } catch (SecretShareException e) { ret = null; } } return ret; } /** * Routine to construct an instance that allows you to print the hex strings. * Such as * String s = BigIntStringChecksum create(biginteger).toString(); * s.equals("bigintcs:00f3ea-CBA3D0"); * * @param in the big integer to hexify and md5 check sum * @return big integer string checksum object */ public static BigIntStringChecksum create(final BigInteger in) { if (in == null) { throw new SecretShareException("Input BigInteger cannot be null"); } final String inHex = in.toString(HEX_RADIX); final String inAsHex = pad(inHex); String md5checksum = computeMd5ChecksumLimit6(inAsHex); return new BigIntStringChecksum(inAsHex, md5checksum); } // ================================================== // constructors // ================================================== /** * Construct an instance of BISC. * Performs NO validation of input. * <p/> * Normally 'private', but Unit Tests need access. * * @param inAsHex just the hex, no dashes * @param inMd5checksum just the hex, no dashes */ /*default*/ BigIntStringChecksum(final String inAsHex, final String inMd5checksum) { asHex = inAsHex; md5checksum = inMd5checksum; } // ================================================== // public methods // ================================================== /** * @return the formatted string that can be parsed back into this object */ @Override public String toString() { String hex6perdash = insertDashesIntoHex(asHex); return PREFIX_BIGINT_DASH_CHECKSUM + hex6perdash + "-" + md5checksum; } /** * Return the original BigInteger. * (or throw an exception if something went wrong). * * @return BigInteger or throw exception * @throws SecretShareException if the hex is invalid */ public BigInteger asBigInteger() { try { return new BigInteger(asHex, HEX_RADIX); } catch (NumberFormatException e) { throw new SecretShareException("Invalid input='" + asHex + "'", e); } } // ================================================== // non public methods // ================================================== /* private */ static String bytesToHexString(byte... in) { String ret = ""; for (byte b : in) { ret += byteToHexString(b); } return ret; } private static String lookup[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}; private static String byteToHexString(byte b) { String ret = ""; byte ch = 0x00; ch = (byte) (b & 0xF0); // strip off high ch = (byte) (ch >>> 4); // shift ch = (byte) (ch & 0X0F); // the >>> turned on high bits, get rid of them ret += lookup[(int) ch]; ch = (byte) (b & 0X0F); ret += lookup[(int) ch]; return ret; } private static String insertDashesIntoHex(final String inAsHex) { String ret = ""; int LEN_PER_GROUP = 6; String input = inAsHex; boolean returnIsNegative = false; if (input.startsWith("-")) { returnIsNegative = true; input = input.substring(1); } while ((input.length() % LEN_PER_GROUP) != 0) { input = "0" + input; } String sep = ""; for (int i = 0, n = input.length() / LEN_PER_GROUP; i < n; i++) { ret += sep; sep = "-"; ret += input.substring((i + 0) * LEN_PER_GROUP, (i + 1) * LEN_PER_GROUP); } if (returnIsNegative) { ret = "-" + ret; } return ret; } private static byte[] computeMd5ChecksumFull(String inAsHex2) { Md5Checksummer md5summer = null; md5summer = Md5ChecksummerFactory.create(); if (false) { // Normally, you use a "-D" on the command line to change md5sum class. // This is just for testing over-ride without needing that "-D" argument. // See Md5ChecksummerFactory.create() md5summer = Md5ChecksummerFactory .createFromClassName("import com.aegiswallet.helpers.secretshare.hashchecks.Md5ChecksummerImpl"); } byte[] bytes = md5summer.createMd5Checksum(inAsHex2.toLowerCase().getBytes()); return bytes; } /** * Note: only not-private to allow unit tests access. */ /*private*/ static String computeMd5ChecksumLimit6(String inAsHex2) { byte[] bytes = computeMd5ChecksumFull(inAsHex2); String md5checksum = bytesToHexString(bytes[2], bytes[1], bytes[0]); return md5checksum; } private static void createThrow(String string, String bics) { throw new SecretShareException(string + "(input=" + bics + ")"); } private static String pad(final String inHex) { int LEN_PER_GROUP = DIGITS_PER_GROUP; String useHex = inHex; boolean returnIsNegative = false; if (useHex.startsWith("-")) { useHex = useHex.substring(1); returnIsNegative = true; } String ret = useHex; while ((ret.length() % LEN_PER_GROUP) != 0) { ret = "0" + ret; } if (returnIsNegative) { ret = "-" + ret; } return ret; } }