/******************************************************************************* * $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; import com.aegiswallet.helpers.secretshare.math.BigIntStringChecksum; import com.aegiswallet.helpers.secretshare.math.CombinationGenerator; import com.aegiswallet.helpers.secretshare.math.EasyLinearEquation; import com.aegiswallet.helpers.secretshare.math.PolyEquationImpl; import java.io.PrintStream; import java.math.BigInteger; import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.UUID; /** * Main class for the "Shamir's Secret Sharing" implementation. * <p/> * General description: * A secret is divided into "n" pieces of data, * such that "k" of those pieces can be used to reconstruct the secret, * but "k-1" of those pieces gets you nothing. * <p/> * The polynomials generated will be order "k-1", * e.g. if k=3, then f(x) = secret + a*x^1 + b*x^2 * and there will be "k" of those polynomials, * each with random [and discarded] coefficients 'a', 'b', etc. * * @author tiemens */ public class SecretShare { // ================================================== // class static data // ================================================== // ================================================== // class static methods // ================================================== /** * http://www.cromwell-intl.com/security/crypto/diffie-hellman.html says * "... choosing some prime p which is larger than the largest possible secret key". * <p/> * Sadly, "larger" is not enough for this implementation [for an unknown reason]. * <p/> * So, some test data shows: * for a secret with 103 bits, 115 is not enough 116 is enough * for a secret with 159 bits, 160 is enough * <p/> * So - provide this method to provide guidance on the modulus to use * for a given secret. * * @param secret number * @return a modulus that (should) work in this library. * It may be larger than it needs to be, but it will work. */ public static BigInteger createAppropriateModulusForSecret(BigInteger secret) { final BigInteger ret; final int originalBitLength = secret.bitLength(); //System.out.println("Creating appropriate for bits=" + originalBitLength); // // be conservative 192 bits -> 180 cutoff // 384 bits -> 370 cutoff // 4096 bits -> 4024 cutoff // if (originalBitLength < 180) { ret = getPrimeUsedFor192bitSecretPayload(); } else if (originalBitLength < 370) { ret = getPrimeUsedFor384bitSecretPayload(); } else if (originalBitLength < 4024) { ret = getPrimeUsedFor4096bigSecretPayload(); } else { // // if you make it here, you are 4000+ bits big. // and that probablePrime() call is going to be really expensive // final int numberOfBitsBigger = originalBitLength / 5; final int numbits = originalBitLength + numberOfBitsBigger; //System.out.println("Secret.bits=" + originalBitLength + " modulus.bits=" + numbits); Random random = new SecureRandom(); // This could take a really long time.... ret = BigInteger.probablePrime(numbits, random); } return ret; } public static boolean isTheModulusAppropriateForSecret(BigInteger modulus, BigInteger secret) { try { checkThatModulusIsAppropriate(modulus, secret); return true; } catch (SecretShareException e) { return false; } } public static void checkThatModulusIsAppropriate(BigInteger primeModulus, BigInteger secret) throws SecretShareException { if (secret.compareTo(primeModulus) >= 0) { throw new SecretShareException("Secret cannot be larger than modulus. " + "Secret=" + secret + "\n" + "Modulus=" + primeModulus); } // Question - look at other rules? } // All primes were tested via http://www.alpertron.com.ar/ECM.HTM // All primes were tested with 100,000 iterations of Miller-Rabin public static BigInteger getPrimeUsedFor4096bigSecretPayload() { // This big integer was created with probablePrime(BigInteger.valueOf(2L).pow(4100)).nextProbablePrime() // It took 28 seconds to generate [Run on Core i7 920 2.67Ghz] // It took 25 minutes to check using alpertron.com.ar applet. [Run on Core2Duo E8500 3.16GHz CPU] BigInteger p4096one = new BigInteger( "1671022210261044010706804337146599012127" + "9427984758140486147735732543262527544919" + "3095812289909599609334542417074310282054" + "0780117501097269771621177740562184444713" + "5311624699359973445785442150139493030849" + "1201896951396220211014303634039307573549" + "4951338587994892653929285926514054477984" + "1897745831487644537568464106991023630108" + "6045751504900830441750495932712549251755" + "0884842714308894440025555839788342744866" + "7101368958164663781091806630951947745404" + "9899622319436016030246615841346729868014" + "9869334160881652755341231281231973786191" + "0590928243420749213395009469338508019541" + "0958855418900088036159728065975165578015" + "3079187511387238090409461192977321170936" + "6081401737953645348323163171237010704282" + "8481068031277612787461827099245660019965" + "4423851454616735972464821439378482870833" + "7709298145449348366148476664877596527269" + "1765522730435723049823184958030880339674" + "1433100452606317504985611860713079871716" + "8809146278034477061142090096734446658190" + "8273334857030516871663995504285034522155" + "7158160427604895839673593745279150722839" + "3997083495197879290548002853265127569910" + "9306488129210915495451479419727501586051" + "1232507931203905482587057398637416125459" + "0876872367709717423642369650017374448020" + "8386154750356267714638641781056467325078" + "08534977443900875333446450467047221" ); // No, these "0"s are not an error. // The nextProbablePrime is only "735(hex)" away from 2^4100... // In case you are wondering, this duplicate-encoding is a guard // against an accidental change in (either) string. String bigintcs = "bigintcs:100000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000000-000000-000000-" + "000000-000000-000000-000000-000735-4C590B"; // return p4096one; return checkAndReturn("4096bit prime", p4096one, bigintcs); } public static BigInteger getPrimeUsedFor384bitSecretPayload() { // This big integer was created with probablePrime(386-bits) // This prime is bigger than 2^384 BigInteger p194one = new BigInteger("830856716641269388050926147210" + "378437007763661599988974204336" + "741171904442622602400099072063" + "84693584652377753448639527"); String bigintcs = "bigintcs:000002-1bd189-52959f-874f79-3d6cf5-11ac82-e6cea4-46c19c-5f523a-5318c7-" + "e0f379-66f9e1-308c61-2d8d0b-dba253-6f54b0-ec6c27-3198DB"; return checkAndReturn("384bit prime", p194one, bigintcs); } public static BigInteger getPrimeUsedFor192bitSecretPayload() { // This big integer was created with probablePrime(194-bits) // This prime is bigger than 2^192 BigInteger p194one = new BigInteger("14976407493557531125525728362448106789840013430353915016137"); String bigintcs = "bigintcs:000002-62c8fd-6ec81b-3c0584-136789-80ad34-9269af-da237f-8ff3c9-12BCCD"; return checkAndReturn("192bit prime", p194one, bigintcs); } /** * Guard against accidental changes to the strings. */ private static BigInteger checkAndReturn(String which, BigInteger expected, String asbigintcs) { BigInteger other = BigIntStringChecksum.fromString(asbigintcs).asBigInteger(); if (expected.equals(other)) { return expected; } else { throw new SecretShareException(which + " failure"); } } // ================================================== // instance data // ================================================== private final PublicInfo publicInfo; public SecretShare(final PublicInfo inPublicInfo) { publicInfo = inPublicInfo; } // ================================================== // public methods // ================================================== /** * Split the secret into pieces. * * @param secret to split * @return split secret output instance */ public SplitSecretOutput split(final BigInteger secret) { return split(secret, new SecureRandom()); } /** * Split the secret into pieces, where the caller controls the random instance. * * @param secret to split * @param random to use for random number generation * @return split secret output instance */ public SplitSecretOutput split(final BigInteger secret, final Random random) { if (secret == null) { throw new SecretShareException("Secret cannot be null"); } if (secret.signum() <= 0) { throw new SecretShareException("Secret cannot be negative"); } if (publicInfo.getPrimeModulus() != null) { checkThatModulusIsAppropriate(publicInfo.getPrimeModulus(), secret); } BigInteger[] coeffs = new BigInteger[publicInfo.getK()]; // create the equation by setting the coefficients: // [a] randomize the coefficients: randomizeCoeffs(coeffs, random, publicInfo.getPrimeModulus(), secret); // [b] set the constant coefficient to the secret: coeffs[0] = secret; final PolyEquationImpl equation = new PolyEquationImpl(coeffs); SplitSecretOutput ret = new SplitSecretOutput(this.publicInfo, equation); for (int x = 1, n = publicInfo.getN() + 1; x < n; x++) { final BigInteger fofx = equation.calculateFofX(BigInteger.valueOf(x)); BigInteger data = fofx; if (publicInfo.primeModulus != null) { data = data.mod(publicInfo.primeModulus); } final ShareInfo share = new ShareInfo(x, data, this.publicInfo); ret.sharesInfo.add(share); } return ret; } /** * Combine the shares generated by the split to recover the secret. * * @param usetheseshares shares to use * @return the combine output instance [which in turn contains the recovered secret] */ public CombineOutput combine(final List<ShareInfo> usetheseshares) { CombineOutput ret = null; if (publicInfo.getK() > usetheseshares.size()) { throw new SecretShareException("Must have " + publicInfo.getK() + " shares to solve. Only provided " + usetheseshares.size()); } checkForDuplicatesOrThrow(usetheseshares); final int size = publicInfo.getK(); BigInteger[] xarray = new BigInteger[size]; BigInteger[] fofxarray = new BigInteger[size]; for (int i = 0, n = size; i < n; i++) { xarray[i] = usetheseshares.get(i).getXasBigInteger(); fofxarray[i] = usetheseshares.get(i).getShare(); } EasyLinearEquation ele = EasyLinearEquation.createForPolynomial(xarray, fofxarray); if (publicInfo.getPrimeModulus() != null) { ele = ele.createWithPrimeModulus(publicInfo.getPrimeModulus()); } EasyLinearEquation.EasySolve solve = ele.solve(); BigInteger solveSecret = solve.getAnswer(1); if (publicInfo.getPrimeModulus() != null) { solveSecret = solveSecret.mod(publicInfo.getPrimeModulus()); } ret = new CombineOutput(solveSecret); return ret; } private void checkForDuplicatesOrThrow(List<ShareInfo> shares) { Set<ShareInfo> seen = new HashSet<ShareInfo>(); for (ShareInfo s : shares) { if (seen.contains(s)) { throw new SecretShareException("Duplicate share of " + s.debugDump()); } else { seen.add(s); } } } // ================================================== // private methods // ================================================== private void randomizeCoeffs(final BigInteger[] coeffs, final Random random, final BigInteger modulus, final BigInteger secret) { for (int i = 1, n = coeffs.length; i < n; i++) { BigInteger big = null; //big = BigInteger.valueOf((random.nextInt() % 20) + 1); big = BigInteger.valueOf(random.nextLong()); // ENHANCEMENT: provide better control? make it even bigger? // for now, we'll just do long^2: big = big.multiply(BigInteger.valueOf(random.nextLong())); big = big.abs(); // make it positive coeffs[i] = big; // Book says "all coefficients are smaller than the modulus" if (modulus != null) { coeffs[i] = coeffs[i].mod(modulus); } coeffs[i] = coeffs[i].mod(secret); } } // ================================================== // public // ================================================== /** * Holds all the "publicly available" information about a secret share. * Holds both "required" and "optional" information. */ public static class PublicInfo { // the required public info: "K" and the modulus private final int k; // determines the order of the polynomial private final BigInteger primeModulus; // can be null // useful information: "N" - how many shares were generated? private final int n; // just descriptive info: private final String description; // any string, including null private final String uuid; // a "Random" UUID string private final String date; // yyyy-MM-dd HH:mm:ss string public PublicInfo(final int inN, final int inK, final BigInteger inPrimeModulus, final String inDescription) { super(); this.n = inN; this.k = inK; this.primeModulus = inPrimeModulus; this.description = inDescription; UUID uuidobj = UUID.randomUUID(); uuid = uuidobj.toString(); date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); if (k > n) { throw new SecretShareException("k cannot be bigger than n [k=" + k + " n=" + n + "]"); } } @Override public String toString() { return "PublicInfo[k=" + k + ", n=" + n + "\n" + "modulus=" + primeModulus + "\n" + "description=" + description + "\n" + "date=" + date + "\n" + "uuid=" + uuid + "]"; } public String debugDump() { return toString(); } public final int getN() { return n; } public final int getK() { return k; } public final BigInteger getPrimeModulus() { return primeModulus; } public final String getDescription() { return description; } public final String getUuid() { return uuid; } public final String getDate() { return date; } } /** * Holds all the info needed to be a "piece" of the secret. * aka a "Share" of the secret. * * @author tiemens */ public static class ShareInfo { // Identity fields: private final int x; // this is aka "the index", the x in "f(x)" private final BigInteger share; // our piece of the secret // "extra" private final PublicInfo publicInfo; public ShareInfo(final int inX, final BigInteger inShare, final PublicInfo inPublicInfo) { if (inShare == null) { throw new SecretShareException("share cannot be null"); } if (inPublicInfo == null) { throw new SecretShareException("publicinfo cannot be null"); } x = inX; share = inShare; publicInfo = inPublicInfo; } public String debugDump() { return "ShareInfo[x=" + x + "\n" + "share=" + share + " public=" + publicInfo.debugDump() + "]"; } public final int getIndex() { return x; } public final int getX() { return x; } public final BigInteger getXasBigInteger() { return BigInteger.valueOf(x); } public final BigInteger getShare() { return share; } public final PublicInfo getPublicInfo() { return publicInfo; } @Override public int hashCode() { return x; } @Override public boolean equals(Object obj) { if (obj instanceof ShareInfo) { return equalsType((ShareInfo) obj); } else { return false; } } public boolean equalsType(ShareInfo other) { // NOTE: the "public info" does not count toward identity return ((this.x == other.x) && (this.share.equals(other.share))); } } /** * When the secret is split, this is the information that is returned. * Note: This is NOT the "public" information, since the polynomial * used in splitting the secret is in this object. * The "public" information is the '.getShareInfos()' method. */ public static class SplitSecretOutput { private final PublicInfo publicInfo; private final List<ShareInfo> sharesInfo = new ArrayList<ShareInfo>(); private final PolyEquationImpl polynomial; public SplitSecretOutput(final PublicInfo inPublicInfo, final PolyEquationImpl inPolynomial) { publicInfo = inPublicInfo; polynomial = inPolynomial; } public String debugDump() { String ret = "Public=" + publicInfo.debugDump() + "\n"; ret += "EQ: " + polynomial.debugDump() + "\n"; for (ShareInfo share : sharesInfo) { ret += "SHARE: " + share.debugDump() + "\n"; } return ret; } public final List<ShareInfo> getShareInfos() { return Collections.unmodifiableList(sharesInfo); } public final PublicInfo getPublicInfo() { return publicInfo; } } /** * Holds the output of the combine() operation, i.e. the original secret. */ public static class CombineOutput { private final BigInteger secret; public CombineOutput(final BigInteger inSecret) { secret = inSecret; } public final BigInteger getSecret() { return secret; } } public BigInteger combineParanoid(List<ShareInfo> shares) { return combineParanoid(shares, null, System.out); } public BigInteger combineParanoid(List<ShareInfo> shares, Integer maximumCombinationsToTest, PrintStream outstream) { BigInteger answer = null; PublicInfo publicInfo = shares.get(0).getPublicInfo(); CombinationGenerator<ShareInfo> combo = new CombinationGenerator<ShareInfo>(shares, publicInfo.getK()); if (outstream != null) { outstream.println("SecretShare.paranoid(max=" + maximumCombinationsToTest + " combo.total=" + combo.getTotalNumberOfCombinations() + ")"); } final int percentEvery = 30; // or 10 for every 10% int outputEvery = 100; if (maximumCombinationsToTest != null) { if (BigInteger.valueOf(maximumCombinationsToTest) .compareTo(combo.getTotalNumberOfCombinations()) > 0) { maximumCombinationsToTest = combo.getTotalNumberOfCombinations().intValue(); outputEvery = (maximumCombinationsToTest * percentEvery) / 100 + 1; } } else { outputEvery = (combo.getTotalNumberOfCombinations().intValue() * percentEvery) / 100 + 1; } int count = -1; for (List<ShareInfo> usetheseshares : combo) { count++; if (maximumCombinationsToTest != null) { if (count > maximumCombinationsToTest) { break; } } if ((count % outputEvery) == 0) { if (outstream != null) { outstream.println("Combination: " + combo.getCurrentCombinationNumber() + " of " + combo.getTotalNumberOfCombinations() + combo.getIndexesAsString() + dumpshares(usetheseshares)); } } CombineOutput solved = this.combine(usetheseshares); BigInteger solve = solved.getSecret(); if (answer == null) { answer = solve; } else { if (!answer.equals(solve)) { throw new SecretShareException("Paranoid test failed, on combination at count=" + count); } } } return answer; } private String dumpshares(List<ShareInfo> usetheseshares) { String ret = ""; for (ShareInfo share : usetheseshares) { ret += " " + share.getShare(); } return ret; } }