diff --git a/src/main/java/jota/IotaAPI.java b/src/main/java/jota/IotaAPI.java index ad1353d..514269c 100644 --- a/src/main/java/jota/IotaAPI.java +++ b/src/main/java/jota/IotaAPI.java @@ -41,13 +41,19 @@ public class IotaAPI extends IotaAPICore { * This is either done deterministically, or by providing the index of the new remainderAddress * * @param seed Tryte-encoded seed. It should be noted that this seed is not transferred + * @param security Security level to be used for the private key / address. Can be 1, 2 or 3 * @param index Optional (default null). Key index to start search from. If the index is provided, the generation of the address is not deterministic. * @param checksum Optional (default false). Adds 9-tryte address checksum * @param total Optional (default 1)Total number of addresses to generate * @param returnAll If true, it returns all addresses which were deterministically generated (until findTransactions returns null) * @return an array of strings with the specifed number of addresses */ - public GetNewAddressResponse getNewAddress(final String seed, final int index, final boolean checksum, final int total, final boolean returnAll) { + public GetNewAddressResponse getNewAddress(final String seed, int security, final int index, final boolean checksum, final int total, final boolean returnAll) throws InvalidSecurityLevelException { + + if (security < 1 || security > 3) { + throw new InvalidSecurityLevelException(); + } + StopWatch stopWatch = new StopWatch(); List allAddresses = new ArrayList<>(); @@ -56,7 +62,7 @@ public class IotaAPI extends IotaAPICore { // and return the list of all addresses if (total != 0) { for (int i = index; i < index + total; i++) { - allAddresses.add(IotaAPIUtils.newAddress(seed, i, checksum, customCurl)); + allAddresses.add(IotaAPIUtils.newAddress(seed, security, i, checksum, customCurl)); } return GetNewAddressResponse.create(allAddresses, stopWatch.getElapsedTimeMili()); } @@ -64,7 +70,7 @@ public class IotaAPI extends IotaAPICore { // already created if null, return list of addresses for (int i = index; ; i++) { - final String newAddress = IotaAPIUtils.newAddress(seed, i, checksum, customCurl); + final String newAddress = IotaAPIUtils.newAddress(seed, security, i, checksum, customCurl); final FindTransactionResponse response = findTransactionsByAddresses(newAddress); allAddresses.add(newAddress); @@ -83,22 +89,26 @@ public class IotaAPI extends IotaAPICore { } /** - * @param {string} seed - * @param {object} options - * @param {function} callback - * @method getTransfers - * @property {int} start Starting key index - * @property {int} end Ending key index - * @property {bool} inclusionStates returns confirmation status of all transactions - * @returns {object} success + * @param seed + * @param start + * @param end + * @param inclusionStates + * @param security + * @param seed + * @param seed + * @returns Bundle **/ - public GetTransferResponse getTransfers(String seed, Integer start, Integer end, Boolean inclusionStates) throws ArgumentException, InvalidBundleException, InvalidSignatureException, NoNodeInfoException, NoInclusionStatesExcpection { + public GetTransferResponse getTransfers(String seed, int security, Integer start, Integer end, Boolean inclusionStates) throws ArgumentException, InvalidBundleException, InvalidSignatureException, NoNodeInfoException, NoInclusionStatesExcpection, InvalidSecurityLevelException { StopWatch stopWatch = new StopWatch(); // validate & if needed pad seed if ((seed = InputValidator.validateSeed(seed)) == null) { throw new IllegalStateException("Invalid Seed"); } + if (security < 1 || security > 3) { + throw new InvalidSecurityLevelException(); + } + start = start != null ? 0 : start; if (start > end || end > (start + 500)) { @@ -107,7 +117,7 @@ public class IotaAPI extends IotaAPICore { StopWatch sw = new StopWatch(); System.out.println("GetTransfer started"); - GetNewAddressResponse gnr = getNewAddress(seed, start, false, end == null ? end - start : end, true); + GetNewAddressResponse gnr = getNewAddress(seed, security, start, false, end == null ? end - start : end, true); if (gnr != null && gnr.getAddresses() != null) { System.out.println("GetTransfers after getNewAddresses " + sw.getElapsedTimeMili() + " ms"); Bundle[] bundles = bundlesFromAddresses(gnr.getAddresses().toArray(new String[gnr.getAddresses().size()]), inclusionStates); @@ -302,17 +312,15 @@ public class IotaAPI extends IotaAPICore { /** * Prepares transfer by generating bundle, finding and signing inputs * - * @param {string} seed - * @param {object} transfers - * @param {object} options - * @param {function} callback - * @return - * @method prepareTransfers - * @property {array} inputs Inputs used for signing. Needs to have correct keyIndex and address value - * @property {string} address Remainder address + * @param seed + * @param security + * @param transfers + * @param remainder + * @param inputs + * @param security * @returns {array} trytes Returns bundle trytes **/ - public List prepareTransfers(String seed, final List transfers, String remainder, List inputs) throws NotEnoughBalanceException { + public List prepareTransfers(String seed, int security, final List transfers, String remainder, List inputs) throws NotEnoughBalanceException, InvalidSecurityLevelException { // Input validation of transfers object if (!InputValidator.isTransfersCollectionCorrect(transfers)) { @@ -324,6 +332,9 @@ public class IotaAPI extends IotaAPICore { throw new IllegalStateException("Invalid Seed"); } + if (security < 1 || security > 3) { + throw new InvalidSecurityLevelException(); + } // Create a new bundle final Bundle bundle = new Bundle(); @@ -403,13 +414,19 @@ public class IotaAPI extends IotaAPICore { int i = 0; for (String balance : balances) { long thisBalance = Integer.parseInt(balance); - totalBalance += thisBalance; // If input has balance, add it to confirmedInputs if (thisBalance > 0) { + totalBalance += thisBalance; Input inputEl = inputs.get(i++); inputEl.setBalance(thisBalance); confirmedInputs.add(inputEl); + + // if we've already reached the intended input value, break out of loop + if (totalBalance >= totalValue) { + log.info("Total balance already reached "); + break; + } } } @@ -418,7 +435,7 @@ public class IotaAPI extends IotaAPICore { throw new IllegalStateException("Not enough balance"); } - return addRemainder(seed, confirmedInputs, bundle, tag, totalValue, null, signatureFragments); + return addRemainder(seed, security, confirmedInputs, bundle, tag, totalValue, null, signatureFragments); } // Case 2: Get inputs deterministically @@ -427,9 +444,9 @@ public class IotaAPI extends IotaAPICore { // confirm that the inputs exceed the threshold else { - @SuppressWarnings("unchecked") GetBalancesAndFormatResponse newinputs = getInputs(seed, 0, 0, totalValue); + @SuppressWarnings("unchecked") GetBalancesAndFormatResponse newinputs = getInputs(seed, security, 0, 0, totalValue); // If inputs with enough balance - return addRemainder(seed, newinputs.getInput(), bundle, tag, totalValue, null, signatureFragments); + return addRemainder(seed, security, newinputs.getInput(), bundle, tag, totalValue, null, signatureFragments); } } else { @@ -451,15 +468,13 @@ public class IotaAPI extends IotaAPICore { /** * Gets the inputs of a seed * - * @param {string} seed - * @param {object} options - * @param {function} callback - * @method getInputs - * @property {int} start Starting key index - * @property {int} end Ending key index - * @property {int} threshold Min balance required + * @param seed + * @param security security secuirty level of private key / seed + * @param start start Starting key index + * @param end end Ending key index + * @param threshold threshold Min balance required **/ - public GetBalancesAndFormatResponse getInputs(String seed, int start, int end, long threshold) { + public GetBalancesAndFormatResponse getInputs(String seed, int security, int start, int end, long threshold) throws InvalidSecurityLevelException { StopWatch stopWatch = new StopWatch(); // validate the seed if (!InputValidator.isTrytes(seed, 0)) { @@ -471,6 +486,10 @@ public class IotaAPI extends IotaAPICore { throw new IllegalStateException("Invalid Seed"); } + if (security < 1 || security > 3) { + throw new InvalidSecurityLevelException(); + } + // If start value bigger than end, return error // or if difference between end and start is bigger than 500 keys if (start > end || end > (start + 500)) { @@ -487,11 +506,11 @@ public class IotaAPI extends IotaAPICore { for (int i = start; i < end; i++) { - String address = IotaAPIUtils.newAddress(seed, i, false, customCurl); + String address = IotaAPIUtils.newAddress(seed, security, i, false, customCurl); allAddresses.add(address); } - return getBalanceAndFormat(allAddresses, threshold, start, end, stopWatch); + return getBalanceAndFormat(allAddresses, threshold, start, end, stopWatch, security); } // Case 2: iterate till threshold || end // @@ -499,14 +518,18 @@ public class IotaAPI extends IotaAPICore { // Calls getNewAddress and deterministically generates and returns all addresses // We then do getBalance, format the output and return it else { - final GetNewAddressResponse res = getNewAddress(seed, start, false, 0, true); - return getBalanceAndFormat(res.getAddresses(), threshold, start, end, stopWatch); + final GetNewAddressResponse res = getNewAddress(seed, security, start, false, 0, true); + return getBalanceAndFormat(res.getAddresses(), threshold, start, end, stopWatch, security); } } // Calls getBalances and formats the output // returns the final inputsObject then - public GetBalancesAndFormatResponse getBalanceAndFormat(final List addresses, long threshold, int start, int end, StopWatch stopWatch) { + public GetBalancesAndFormatResponse getBalanceAndFormat(final List addresses, long threshold, int start, int end, StopWatch stopWatch, int security) throws InvalidSecurityLevelException { + + if (security < 1 || security > 3) { + throw new InvalidSecurityLevelException(); + } GetBalancesResponse getBalancesResponse = getBalances(100, addresses); List balances = Arrays.asList(getBalancesResponse.getBalances()); @@ -524,7 +547,7 @@ public class IotaAPI extends IotaAPICore { long balance = Long.parseLong(balances.get(++i)); if (balance > 0) { - final Input newEntry = new Input(address, balance, start + i); + final Input newEntry = new Input(address, balance, start + i, security); inputs.add(newEntry); // Increase totalBalance of all aggregated inputs @@ -680,10 +703,15 @@ public class IotaAPI extends IotaAPICore { return getInclusionStates(hashes, latestMilestone); } - public SendTransferResponse sendTransfer(String seed, int depth, int minWeightMagnitude, final List transfers, Input[] inputs, String address) throws NotEnoughBalanceException { + public SendTransferResponse sendTransfer(String seed, int security, int depth, int minWeightMagnitude, final List transfers, Input[] inputs, String address) throws NotEnoughBalanceException, InvalidSecurityLevelException { + + if (security < 1 || security > 3) { + throw new InvalidSecurityLevelException(); + } + StopWatch stopWatch = new StopWatch(); - List trytes = prepareTransfers(seed, transfers, address, inputs == null ? null : Arrays.asList(inputs)); + List trytes = prepareTransfers(seed, security, transfers, address, inputs == null ? null : Arrays.asList(inputs)); List trxs = sendTrytes(trytes.toArray(new String[trytes.size()]), depth, minWeightMagnitude); Boolean[] successful = new Boolean[trxs.size()]; @@ -768,12 +796,13 @@ public class IotaAPI extends IotaAPICore { } public List addRemainder(final String seed, + final int security, final List inputs, final Bundle bundle, final String tag, final long totalValue, final String remainderAddress, - final List signatureFragments) throws NotEnoughBalanceException { + final List signatureFragments) throws NotEnoughBalanceException, InvalidSecurityLevelException { long totalTransferValue = totalValue; for (int i = 0; i < inputs.size(); i++) { @@ -782,7 +811,7 @@ public class IotaAPI extends IotaAPICore { long timestamp = (long) Math.floor(Calendar.getInstance().getTimeInMillis() / 1000); // Add input as bundle entry - bundle.addEntry(2, inputs.get(i).getAddress(), toSubtract, tag, timestamp); + bundle.addEntry(security, inputs.get(i).getAddress(), toSubtract, tag, timestamp); // If there is a remainder value // Add extra output to send remaining funds to @@ -799,7 +828,7 @@ public class IotaAPI extends IotaAPICore { } else if (remainder > 0) { // Generate a new Address by calling getNewAddress - GetNewAddressResponse res = getNewAddress(seed, 0, false, 0, false); + GetNewAddressResponse res = getNewAddress(seed, security, 0, false, 0, false); // Remainder bundle entry bundle.addEntry(1, res.getAddresses().get(0), remainder, tag, timestamp); diff --git a/src/main/java/jota/error/InvalidSecurityLevelException.java b/src/main/java/jota/error/InvalidSecurityLevelException.java new file mode 100644 index 0000000..263ffbd --- /dev/null +++ b/src/main/java/jota/error/InvalidSecurityLevelException.java @@ -0,0 +1,11 @@ +package jota.error; + +/** + * Created by pinpong on 02.02.17. + */ +public class InvalidSecurityLevelException extends BaseException { + + public InvalidSecurityLevelException() { + super("Invalid security level"); + } +} diff --git a/src/main/java/jota/model/Input.java b/src/main/java/jota/model/Input.java index 16ce445..ead850c 100644 --- a/src/main/java/jota/model/Input.java +++ b/src/main/java/jota/model/Input.java @@ -10,11 +10,15 @@ public class Input { private String address; private long balance; private int keyIndex; + private int security; - public Input(String address, long balance, int keyIndex) { + + public Input(String address, long balance, int keyIndex, int security) { this.address = address; this.balance = balance; this.keyIndex = keyIndex; + this.security = security; + } @Override @@ -45,4 +49,12 @@ public class Input { public void setKeyIndex(int keyIndex) { this.keyIndex = keyIndex; } + + public int getSecurity() { + return security; + } + + public void setSecurity(int security) { + this.security = security; + } } \ No newline at end of file diff --git a/src/main/java/jota/utils/IotaAPIUtils.java b/src/main/java/jota/utils/IotaAPIUtils.java index 3d59c4c..adbf974 100644 --- a/src/main/java/jota/utils/IotaAPIUtils.java +++ b/src/main/java/jota/utils/IotaAPIUtils.java @@ -4,8 +4,6 @@ import jota.model.Bundle; import jota.model.Input; import jota.model.Transaction; import jota.pow.ICurl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; @@ -19,19 +17,18 @@ import java.util.List; */ public class IotaAPIUtils { - private static final Logger log = LoggerFactory.getLogger(IotaAPIUtils.class); - /** * Generates a new address * * @param seed * @param index + * @param security * @param checksum * @return an String with address */ - public static String newAddress(String seed, int index, boolean checksum, ICurl curl) { + public static String newAddress(String seed, int security, int index, boolean checksum, ICurl curl) { Signing signing = new Signing(curl); - final int[] key = signing.key(Converter.trits(seed), index, 2); + final int[] key = signing.key(Converter.trits(seed), index, security); final int[] digests = signing.digests(key); final int[] addressTrits = signing.address(digests); @@ -47,6 +44,7 @@ public class IotaAPIUtils { final List inputs, final Bundle bundle, final List signatureFragments, ICurl curl) { + bundle.finalize(curl); bundle.addTrytes(signatureFragments); @@ -61,17 +59,18 @@ public class IotaAPIUtils { // Get the corresponding keyIndex of the address int keyIndex = 0; + int keySecurity = 0; for (Input input : inputs) { if (input.getAddress().equals(thisAddress)) { keyIndex = input.getKeyIndex(); - break; + keySecurity = input.getSecurity(); } } String bundleHash = bundle.getTransactions().get(i).getBundle(); // Get corresponding private key of address - int[] key = new Signing(curl).key(Converter.trits(seed), keyIndex, 2); + int[] key = new Signing(curl).key(Converter.trits(seed), keyIndex, keySecurity); // First 6561 trits for the firstFragment int[] firstFragment = Arrays.copyOfRange(key, 0, 6561); @@ -88,22 +87,27 @@ public class IotaAPIUtils { // Convert signature to trytes and assign the new signatureFragment bundle.getTransactions().get(i).setSignatureFragments(Converter.trytes(firstSignedFragment)); - // Because the signature is > 2187 trytes, we need to - // find the second transaction to add the remainder of the signature - for (int j = 0; j < bundle.getTransactions().size(); j++) { - // Same address as well as value = 0 (as we already spent the input) - if (bundle.getTransactions().get(j).getAddress().equals(thisAddress) && Long.parseLong(bundle.getTransactions().get(j).getValue()) == 0) { - // Use the second 6562 trits - int[] secondFragment = Arrays.copyOfRange(key, 6561, 6561 * 2); + // if user chooses higher than 27-tryte security + // for each security level, add an additional signature + for (int j = 1; j < keySecurity; j++) { - // The second 27 to 54 trytes of the bundle hash - int[] secondBundleFragment = Arrays.copyOfRange(normalizedBundleHash, 27, 27 * 2); + // Because the signature is > 2187 trytes, we need to + // find the second transaction to add the remainder of the signature + for (int k = 0; k < bundle.getTransactions().size(); k++) { + // Same address as well as value = 0 (as we already spent the input) + if (bundle.getTransactions().get(k).getAddress().equals(thisAddress) && Long.parseLong(bundle.getTransactions().get(k).getValue()) == 0) { + // Use the second 6562 trits + int[] secondFragment = Arrays.copyOfRange(key, 6561, 6561 * 2); - // Calculate the new signature - int[] secondSignedFragment = new Signing(curl).signatureFragment(secondBundleFragment, secondFragment); + // The second 27 to 54 trytes of the bundle hash + int[] secondBundleFragment = Arrays.copyOfRange(normalizedBundleHash, 27, 27 * 2); - // Convert signature to trytes and assign it again to this bundle entry - bundle.getTransactions().get(j).setSignatureFragments(Converter.trytes(secondSignedFragment)); + // Calculate the new signature + int[] secondSignedFragment = new Signing(curl).signatureFragment(secondBundleFragment, secondFragment); + + // Convert signature to trytes and assign it again to this bundle entry + bundle.getTransactions().get(k).setSignatureFragments(Converter.trytes(secondSignedFragment)); + } } } } diff --git a/src/test/java/jota/IotaAPITest.java b/src/test/java/jota/IotaAPITest.java index 7fc438c..7928694 100644 --- a/src/test/java/jota/IotaAPITest.java +++ b/src/test/java/jota/IotaAPITest.java @@ -73,8 +73,8 @@ public class IotaAPITest { @Test - public void shouldGetInputs() { - GetBalancesAndFormatResponse res = iotaClient.getInputs(TEST_SEED1, 0, 0, 0); + public void shouldGetInputs() throws InvalidSecurityLevelException { + GetBalancesAndFormatResponse res = iotaClient.getInputs(TEST_SEED1, 2, 0, 0, 0); System.out.println(res); assertThat(res, IsNull.notNullValue()); assertThat(res.getTotalBalance(), IsNull.notNullValue()); @@ -84,20 +84,20 @@ public class IotaAPITest { @Test - public void shouldCreateANewAddress() { - final GetNewAddressResponse res = iotaClient.getNewAddress(TEST_SEED1, 0, false, 100, false); + public void shouldCreateANewAddress() throws InvalidSecurityLevelException { + final GetNewAddressResponse res = iotaClient.getNewAddress(TEST_SEED1, 2, 0, false, 100, false); assertThat(res.getAddresses().get(0), Is.is(TEST_ADDRESS_WITHOUT_CHECKSUM)); System.out.println(new Gson().toJson(res)); } @Test - public void shouldPrepareTransfer() { + public void shouldPrepareTransfer() throws InvalidSecurityLevelException { List transfers = new ArrayList<>(); transfers.add(new jota.model.Transfer(TEST_ADDRESS_WITH_CHECKSUM, 0, TEST_MESSAGE, TEST_TAG)); transfers.add(new jota.model.Transfer(TEST_ADDRESS_WITH_CHECKSUM, 1, TEST_MESSAGE, TEST_TAG)); List trytes = null; try { - trytes = iotaClient.prepareTransfers(TEST_SEED1, transfers, null, null); + trytes = iotaClient.prepareTransfers(TEST_SEED1, 2, transfers, null, null); } catch (NotEnoughBalanceException e) { e.printStackTrace(); } @@ -124,8 +124,8 @@ public class IotaAPITest { } @Test - public void shouldGetTransfers() throws InvalidBundleException, ArgumentException, InvalidSignatureException, NoInclusionStatesExcpection, NoNodeInfoException { - GetTransferResponse gtr = iotaClient.getTransfers(TEST_SEED1, 0, 0, false); + public void shouldGetTransfers() throws InvalidBundleException, ArgumentException, InvalidSignatureException, NoInclusionStatesExcpection, NoNodeInfoException, InvalidSecurityLevelException { + GetTransferResponse gtr = iotaClient.getTransfers(TEST_SEED1, 2, 0, 0, false); assertThat(gtr.getTransfers(), IsNull.notNullValue()); for (Bundle test : gtr.getTransfers()) { @@ -142,19 +142,19 @@ public class IotaAPITest { } @Test(expected = IllegalStateException.class) - public void shouldNotSendTransfer() throws ArgumentException, InvalidSignatureException, InvalidBundleException, NotEnoughBalanceException { + public void shouldNotSendTransfer() throws ArgumentException, InvalidSignatureException, InvalidBundleException, NotEnoughBalanceException, InvalidSecurityLevelException { List transfers = new ArrayList<>(); transfers.add(new jota.model.Transfer(TEST_ADDRESS_WITHOUT_CHECKSUM, 10000990, "JUSTANOTHERTEST", TEST_TAG)); - SendTransferResponse str = iotaClient.sendTransfer(TEST_SEED2, 9, 18, transfers, null, null); + SendTransferResponse str = iotaClient.sendTransfer(TEST_SEED2, 2, 9, 18, transfers, null, null); assertThat(str.getSuccessfully(), IsNull.notNullValue()); } @Ignore @Test - public void shouldSendTransfer() throws ArgumentException, InvalidSignatureException, InvalidBundleException, NotEnoughBalanceException { + public void shouldSendTransfer() throws ArgumentException, InvalidSignatureException, InvalidBundleException, NotEnoughBalanceException, InvalidSecurityLevelException { List transfers = new ArrayList<>(); transfers.add(new jota.model.Transfer(TEST_ADDRESS_WITHOUT_CHECKSUM, 0, "JUSTANOTHERTEST", TEST_TAG)); - SendTransferResponse str = iotaClient.sendTransfer(TEST_SEED2, 9, 18, transfers, null, null); + SendTransferResponse str = iotaClient.sendTransfer(TEST_SEED2, 2, 9, 18, transfers, null, null); assertThat(str.getSuccessfully(), IsNull.notNullValue()); } } \ No newline at end of file