diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0227e65..d04c9066e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps. * deprecated `addTimeBounds()` use `addPreconditions()` instead * deprecated `setTimeout()` use `addPreconditions()` instead * deprecated `Transaction.Builder` use TransactionBuilder instead +* org.stellar.sdk.Transaction + * `getSignatures()` returns an ImmutableList of signatures, do NOT add signatures to the collection returned. + use `addSignature(DecoratedSignature signature)` instead. ## 0.31.0 diff --git a/src/main/java/org/stellar/sdk/AbstractTransaction.java b/src/main/java/org/stellar/sdk/AbstractTransaction.java index 99de3c7cb..500930be0 100644 --- a/src/main/java/org/stellar/sdk/AbstractTransaction.java +++ b/src/main/java/org/stellar/sdk/AbstractTransaction.java @@ -1,7 +1,14 @@ package org.stellar.sdk; +import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; -import org.stellar.sdk.xdr.*; +import org.stellar.sdk.xdr.DecoratedSignature; +import org.stellar.sdk.xdr.Hash; +import org.stellar.sdk.xdr.SignatureHint; +import org.stellar.sdk.xdr.TransactionEnvelope; +import org.stellar.sdk.xdr.TransactionSignaturePayload; +import org.stellar.sdk.xdr.XdrDataInputStream; +import org.stellar.sdk.xdr.XdrDataOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -75,14 +82,34 @@ public String hashHex() { */ public abstract byte[] signatureBase(); + /** + * Gets the Network string for this transaction. + * + * @return the Network string + */ public Network getNetwork() { return mNetwork; } + /** + * Gets read only list(immutable) of the signatures on transaction. + * + * @return immutable list of signatures + */ public List getSignatures() { - return mSignatures; + return ImmutableList.copyOf(mSignatures); } + /** + * Adds an additional signature to the transaction's existing list of signatures. + * + * @param signature the signature to add + */ + public void addSignature(DecoratedSignature signature) { + mSignatures.add(signature); + } + + public abstract TransactionEnvelope toEnvelopeXdr(); /** diff --git a/src/main/java/org/stellar/sdk/KeyPair.java b/src/main/java/org/stellar/sdk/KeyPair.java index 40ff0bfb5..1c71185e9 100644 --- a/src/main/java/org/stellar/sdk/KeyPair.java +++ b/src/main/java/org/stellar/sdk/KeyPair.java @@ -9,8 +9,14 @@ import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; - -import org.stellar.sdk.xdr.*; +import org.stellar.sdk.xdr.DecoratedSignature; +import org.stellar.sdk.xdr.PublicKey; +import org.stellar.sdk.xdr.PublicKeyType; +import org.stellar.sdk.xdr.SignatureHint; +import org.stellar.sdk.xdr.SignerKey; +import org.stellar.sdk.xdr.SignerKeyType; +import org.stellar.sdk.xdr.Uint256; +import org.stellar.sdk.xdr.XdrDataOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -21,6 +27,7 @@ import java.util.Arrays; import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.System.arraycopy; /** * Holds a Stellar keypair. @@ -227,7 +234,9 @@ public byte[] sign(byte[] data) { /** * Sign the provided data with the keypair's private key and returns {@link DecoratedSignature}. - * @param data + * + * @param data the data to sign, typically the tx hash + * @return DecoratedSignature */ public DecoratedSignature signDecorated(byte[] data) { byte[] signatureBytes = this.sign(data); @@ -241,6 +250,34 @@ public DecoratedSignature signDecorated(byte[] data) { return decoratedSignature; } + /** + * Sign the provided payload data for payload signer where the input is the data being signed. + * Per the + * {@link DecoratedSignature}. + * + * @param signerPayload the payload signers raw data to sign + * @return DecoratedSignature + */ + public DecoratedSignature signPayloadDecorated(byte[] signerPayload) { + DecoratedSignature payloadSignature = signDecorated(signerPayload); + + byte[] hint = new byte[4]; + + // copy the last four bytes of the payload into the new hint + if (signerPayload.length >= hint.length) { + arraycopy(signerPayload, signerPayload.length - hint.length, hint, 0, hint.length); + } else { + arraycopy(signerPayload, 0, hint, 0, signerPayload.length); + } + + //XOR the new hint with this keypair's public key hint + for (int i = 0; i < hint.length; i++) { + hint[i] ^= payloadSignature.getHint().getSignatureHint()[i]; + } + payloadSignature.getHint().setSignatureHint(hint); + return payloadSignature; + } + /** * Verify the provided data and signature match this keypair's public key. * @param data The data that was signed. diff --git a/src/test/java/org/stellar/sdk/KeyPairTest.java b/src/test/java/org/stellar/sdk/KeyPairTest.java index 54d087477..2d9b661f4 100644 --- a/src/test/java/org/stellar/sdk/KeyPairTest.java +++ b/src/test/java/org/stellar/sdk/KeyPairTest.java @@ -2,11 +2,15 @@ import org.junit.Assert; import org.junit.Test; +import org.stellar.sdk.xdr.DecoratedSignature; import java.util.HashMap; import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class KeyPairTest { @@ -86,4 +90,26 @@ public void testSignWithoutSecret() { assertEquals("KeyPair does not contain secret key. Use KeyPair.fromSecretSeed method to create a new KeyPair with a secret key.", e.getMessage()); } } + + @Test + public void testSignPayloadSigner() { + KeyPair keypair = KeyPair.fromSecretSeed(Util.hexToBytes(SEED)); + // the hint from this keypair is [254,66,4,55] + + byte[] payload = new byte[]{1,2,3,4,5}; + DecoratedSignature sig = keypair.signPayloadDecorated(payload); + Assert.assertArrayEquals(sig.getHint().getSignatureHint(), new byte[]{(byte)(0xFF & 252), 65, 0, 50}); + + } + + @Test + public void testSignPayloadSignerLessThanHint() { + KeyPair keypair = KeyPair.fromSecretSeed(Util.hexToBytes(SEED)); + // the hint from this keypair is [254,66,4,55] + + byte[] payload = new byte[]{1,2,3}; + DecoratedSignature sig = keypair.signPayloadDecorated(payload); + // the hint could only be derived off of 3 bytes from payload + Assert.assertArrayEquals(sig.getHint().getSignatureHint(), new byte[]{(byte)(255), 64, 7, 55}); + } } diff --git a/src/test/java/org/stellar/sdk/Sep10ChallengeTest.java b/src/test/java/org/stellar/sdk/Sep10ChallengeTest.java index 0e24701c6..aed005b31 100644 --- a/src/test/java/org/stellar/sdk/Sep10ChallengeTest.java +++ b/src/test/java/org/stellar/sdk/Sep10ChallengeTest.java @@ -3,6 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; import org.junit.Test; +import org.stellar.sdk.xdr.DecoratedSignature; import org.stellar.sdk.xdr.EnvelopeType; import org.stellar.sdk.xdr.TransactionEnvelope; @@ -238,7 +239,9 @@ public void testReadChallengeTransactionRejectsMuxedClient() throws InvalidSep10 .addPreconditions(transaction.getPreconditions()) .build(); - withMuxedClient.getSignatures().addAll(transaction.mSignatures); + for (DecoratedSignature signature : transaction.mSignatures) { + withMuxedClient.addSignature(signature); + } try { Sep10Challenge.readChallengeTransaction( diff --git a/src/test/java/org/stellar/sdk/TransactionTest.java b/src/test/java/org/stellar/sdk/TransactionTest.java index e0630739b..1d6c2ee82 100644 --- a/src/test/java/org/stellar/sdk/TransactionTest.java +++ b/src/test/java/org/stellar/sdk/TransactionTest.java @@ -2,6 +2,7 @@ import com.google.common.io.BaseEncoding; import org.junit.Test; +import org.stellar.sdk.xdr.DecoratedSignature; import org.stellar.sdk.xdr.EnvelopeType; import org.stellar.sdk.xdr.SignerKey; import org.stellar.sdk.xdr.XdrDataInputStream; @@ -15,6 +16,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class TransactionTest { @@ -56,6 +58,36 @@ public void testParseV0Transaction() throws FormatException, IOException { } + @Test + public void testAddingSignaturesDirectly() { + KeyPair source = KeyPair.fromAccountId("GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB"); + KeyPair destination = KeyPair.fromAccountId("GDJJRRMBK4IWLEPJGIE6SXD2LP7REGZODU7WDC3I2D6MR37F4XSHBKX2"); + + Account account = new Account(source.getAccountId(), 0L); + + Transaction transaction = new Transaction( + AccountConverter.disableMuxed(), + account.getAccountId(), + Transaction.MIN_BASE_FEE, + account.getIncrementedSequenceNumber(), + new org.stellar.sdk.Operation[]{new CreateAccountOperation.Builder(destination.getAccountId(), "2000").build()}, + null, + new TransactionPreconditions(null, null, 0, 0, new ArrayList(),null), + Network.PUBLIC + ); + + assertEquals(0, transaction.getSignatures().size()); + try { + // should not be able to change the list of signatures directly + transaction.getSignatures().add(new DecoratedSignature()); + fail(); + } catch (UnsupportedOperationException ignored) {} + + // should only be able to add signatures through interface + transaction.addSignature(new DecoratedSignature()); + assertEquals(1, transaction.getSignatures().size()); + } + @Test public void testSha256HashSigning() throws FormatException { KeyPair source = KeyPair.fromAccountId("GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB");