diff --git a/CHANGELOG.md b/CHANGELOG.md index b5783cd59..1c0d75c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps. A breaking change will get clearly notified in this log. +## 0.20.0 +* Update challenge transaction helpers for SEP-0010 v2.1.0. ([#300](https://github.com/stellar/java-stellar-sdk/pull/300)). + - Remove verification of domain name. + - Allow additional manage data operations that have the source account set as the server key. + ## 0.19.0 ### Add diff --git a/src/main/java/org/stellar/sdk/Sep10Challenge.java b/src/main/java/org/stellar/sdk/Sep10Challenge.java index 7ccbbb851..11f182f27 100644 --- a/src/main/java/org/stellar/sdk/Sep10Challenge.java +++ b/src/main/java/org/stellar/sdk/Sep10Challenge.java @@ -19,7 +19,7 @@ public class Sep10Challenge { * @param signer The server's signing account. * @param network The Stellar network used by the server. * @param clientAccountId The stellar account belonging to the client. - * @param domainName The fully qualified domain name of the service requiring authentication. + * @param domainName The fully qualified domain name of the service requiring authentication (The domainName field is reserved for future use and not used). * @param timebounds The lifetime of the challenge token. */ public static Transaction newChallenge( @@ -69,7 +69,7 @@ public static Transaction newChallenge( * @param challengeXdr SEP-0010 transaction challenge transaction in base64. * @param serverAccountId Account ID for server's account. * @param network The network to connect to for verifying and retrieving. - * @param domainName The fully qualified domain name of the service requiring authentication. + * @param domainName The fully qualified domain name of the service requiring authentication (The domainName field is reserved for future use and not used). * @return {@link ChallengeTransaction}, the decoded transaction envelope and client account ID contained within. * @throws InvalidSep10ChallengeException If the SEP-0010 validation fails, the exception will be thrown. * @throws IOException If read XDR string fails, the exception will be thrown. @@ -112,10 +112,12 @@ public static ChallengeTransaction readChallengeTransaction(String challengeXdr, throw new InvalidSep10ChallengeException("Transaction is not within range of the specified timebounds."); } - // verify that transaction contains a single Manage Data operation and its source account is not null - if (transaction.getOperations().length != 1) { - throw new InvalidSep10ChallengeException("Transaction requires a single ManageData operation."); + if (transaction.getOperations().length < 1) { + throw new InvalidSep10ChallengeException("Transaction requires at least one ManageData operation."); } + + // verify that the first operation in the transaction is a Manage Data operation + // and its source account is not null Operation operation = transaction.getOperations()[0]; if (!(operation instanceof ManageDataOperation)) { throw new InvalidSep10ChallengeException("Operation type should be ManageData."); @@ -128,10 +130,6 @@ public static ChallengeTransaction readChallengeTransaction(String challengeXdr, throw new InvalidSep10ChallengeException("Operation should have a source account."); } - if (!String.format("%s %s", domainName, MANAGER_DATA_NAME_FLAG).equals(manageDataOperation.getName())) { - throw new InvalidSep10ChallengeException("The transaction's operation key name does not include the expected home domain."); - } - if (StrKey.decodeVersionByte(clientAccountId) != StrKey.VersionByte.ACCOUNT_ID) { throw new InvalidSep10ChallengeException("clientAccountId: "+clientAccountId+" is not a valid account id"); } @@ -153,6 +151,21 @@ public static ChallengeTransaction readChallengeTransaction(String challengeXdr, throw new InvalidSep10ChallengeException("Random nonce before encoding as base64 should be 48 bytes long."); } + // verify subsequent operations are manage data ops with source account set to server account + for (int i = 1; i < transaction.getOperations().length; i++) { + Operation op = transaction.getOperations()[i]; + if (!(op instanceof ManageDataOperation)) { + throw new InvalidSep10ChallengeException("Operation type should be ManageData."); + } + ManageDataOperation manageDataOp = (ManageDataOperation) op; + if (manageDataOp.getSourceAccount() == null) { + throw new InvalidSep10ChallengeException("Operation should have a source account."); + } + if (!manageDataOp.getSourceAccount().equals(serverAccountId)) { + throw new InvalidSep10ChallengeException("Subsequent operations are unrecognized."); + } + } + if (!verifyTransactionSignature(transaction, serverAccountId)) { throw new InvalidSep10ChallengeException(String.format("Transaction not signed by server: %s.", serverAccountId)); } @@ -172,7 +185,7 @@ public static ChallengeTransaction readChallengeTransaction(String challengeXdr, * @param challengeXdr SEP-0010 transaction challenge transaction in base64. * @param serverAccountId Account ID for server's account. * @param network The network to connect to for verifying and retrieving. - * @param domainName The fully qualified domain name of the service requiring authentication. + * @param domainName The fully qualified domain name of the service requiring authentication (The domainName field is reserved for future use and not used). * @param signers The signers of client account. * @return a list of signers that were found is returned, excluding the server account ID. * @throws InvalidSep10ChallengeException If the SEP-0010 validation fails, the exception will be thrown. @@ -262,7 +275,7 @@ public static Set verifyChallengeTransactionSigners(String challengeXdr, * @param challengeXdr SEP-0010 transaction challenge transaction in base64. * @param serverAccountId Account ID for server's account. * @param network The network to connect to for verifying and retrieving. - * @param domainName The fully qualified domain name of the service requiring authentication. + * @param domainName The fully qualified domain name of the service requiring authentication (The domainName field is reserved for future use and not used). * @param threshold The threshold on the client account. * @param signers The signers of client account. * @return a list of signers that were found is returned, excluding the server account ID. diff --git a/src/test/java/org/stellar/sdk/Sep10ChallengeTest.java b/src/test/java/org/stellar/sdk/Sep10ChallengeTest.java index 02ca66685..15420bea8 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.BumpSequenceOp; import org.stellar.sdk.xdr.EnvelopeType; import org.stellar.sdk.xdr.TransactionEnvelope; @@ -537,9 +538,45 @@ public void testReadChallengeTransactionInvalidTimeBoundsTooLate() throws Invali } @Test - public void testReadChallengeTransactionInvalidTooManyOperations() throws IOException { + public void testReadChallengeTransactionInvalidOperationWrongType() throws IOException { KeyPair server = KeyPair.random(); KeyPair client = KeyPair.random(); + Network network = Network.TESTNET; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + String domainName = "example.com"; + + Account sourceAccount = new Account(server.getAccountId(), -1L); + SetOptionsOperation setOptionsOperation = new SetOptionsOperation.Builder() + .setSourceAccount(client.getAccountId()) + .build(); + + Operation[] operations = new Operation[]{setOptionsOperation}; + Transaction transaction = new Transaction( + sourceAccount.getAccountId(), + 100 * operations.length, + sourceAccount.getIncrementedSequenceNumber(), + operations, + Memo.none(), + timeBounds, + network + ); + transaction.sign(server); + String challenge = transaction.toEnvelopeXdrBase64(); + + try { + Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); + fail(); + } catch (InvalidSep10ChallengeException e) { + assertEquals("Operation type should be ManageData.", e.getMessage()); + } + } + + @Test + public void testReadChallengeTransactionInvalidOperationNoSourceAccount() throws IOException { + KeyPair server = KeyPair.random(); String domainName = "example.com"; Network network = Network.TESTNET; @@ -556,14 +593,9 @@ public void testReadChallengeTransactionInvalidTooManyOperations() throws IOExce Account sourceAccount = new Account(server.getAccountId(), -1L); ManageDataOperation manageDataOperation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) - .setSourceAccount(client.getAccountId()) - .build(); - - ManageDataOperation manageDataOperation2 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) - .setSourceAccount(client.getAccountId()) .build(); - Operation[] operations = new Operation[]{manageDataOperation1, manageDataOperation2}; + Operation[] operations = new Operation[]{manageDataOperation1}; Transaction transaction = new Transaction( sourceAccount.getAccountId(), 100 * operations.length, @@ -580,27 +612,34 @@ public void testReadChallengeTransactionInvalidTooManyOperations() throws IOExce Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); fail(); } catch (InvalidSep10ChallengeException e) { - assertEquals("Transaction requires a single ManageData operation.", e.getMessage()); + assertEquals("Operation should have a source account.", e.getMessage()); } } @Test - public void testReadChallengeTransactionInvalidOperationWrongType() throws IOException { + public void testReadChallengeTransactionInvalidDataValueWrongEncodedLength() throws IOException { KeyPair server = KeyPair.random(); KeyPair client = KeyPair.random(); + String domainName = "example.com"; + Network network = Network.TESTNET; long now = System.currentTimeMillis() / 1000L; long end = now + 300; TimeBounds timeBounds = new TimeBounds(now, end); - String domainName = "example.com"; + + byte[] nonce = new byte[32]; + SecureRandom random = new SecureRandom(); + random.nextBytes(nonce); + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); Account sourceAccount = new Account(server.getAccountId(), -1L); - SetOptionsOperation setOptionsOperation = new SetOptionsOperation.Builder() + ManageDataOperation manageDataOperation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) .setSourceAccount(client.getAccountId()) .build(); - Operation[] operations = new Operation[]{setOptionsOperation}; + Operation[] operations = new Operation[]{manageDataOperation1}; Transaction transaction = new Transaction( sourceAccount.getAccountId(), 100 * operations.length, @@ -617,13 +656,14 @@ public void testReadChallengeTransactionInvalidOperationWrongType() throws IOExc Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); fail(); } catch (InvalidSep10ChallengeException e) { - assertEquals("Operation type should be ManageData.", e.getMessage()); + assertEquals("Random nonce encoded as base64 should be 64 bytes long.", e.getMessage()); } } @Test - public void testReadChallengeTransactionInvalidOperationNoSourceAccount() throws IOException { + public void testReadChallengeTransactionInvalidDataValueCorruptBase64() throws IOException { KeyPair server = KeyPair.random(); + KeyPair client = KeyPair.random(); String domainName = "example.com"; Network network = Network.TESTNET; @@ -632,14 +672,10 @@ public void testReadChallengeTransactionInvalidOperationNoSourceAccount() throws long end = now + 300; TimeBounds timeBounds = new TimeBounds(now, end); - byte[] nonce = new byte[48]; - SecureRandom random = new SecureRandom(); - random.nextBytes(nonce); - BaseEncoding base64Encoding = BaseEncoding.base64(); - byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); - + byte[] encodedNonce = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?AAAAAAAAAAAAAAAAAAAAAAAAAA".getBytes("UTF-8"); Account sourceAccount = new Account(server.getAccountId(), -1L); ManageDataOperation manageDataOperation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + .setSourceAccount(client.getAccountId()) .build(); Operation[] operations = new Operation[]{manageDataOperation1}; @@ -659,12 +695,13 @@ public void testReadChallengeTransactionInvalidOperationNoSourceAccount() throws Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); fail(); } catch (InvalidSep10ChallengeException e) { - assertEquals("Operation should have a source account.", e.getMessage()); + assertEquals("Failed to decode random nonce provided in ManageData operation.", e.getMessage()); + assertEquals("com.google.common.io.BaseEncoding$DecodingException: Unrecognized character: ?", e.getCause().getMessage()); } } @Test - public void testReadChallengeTransactionInvalidDataValueWrongEncodedLength() throws IOException { + public void testReadChallengeTransactionInvalidDataValueWrongByteLength() throws IOException { KeyPair server = KeyPair.random(); KeyPair client = KeyPair.random(); String domainName = "example.com"; @@ -675,7 +712,7 @@ public void testReadChallengeTransactionInvalidDataValueWrongEncodedLength() thr long end = now + 300; TimeBounds timeBounds = new TimeBounds(now, end); - byte[] nonce = new byte[32]; + byte[] nonce = new byte[47]; SecureRandom random = new SecureRandom(); random.nextBytes(nonce); BaseEncoding base64Encoding = BaseEncoding.base64(); @@ -703,29 +740,124 @@ public void testReadChallengeTransactionInvalidDataValueWrongEncodedLength() thr Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); fail(); } catch (InvalidSep10ChallengeException e) { - assertEquals("Random nonce encoded as base64 should be 64 bytes long.", e.getMessage()); + assertEquals("Random nonce before encoding as base64 should be 48 bytes long.", e.getMessage()); } } @Test - public void testReadChallengeTransactionInvalidDataValueCorruptBase64() throws IOException { + public void testReadChallengeTransactionValidDoesNotVerifyHomeDomainWithHomeDomainSetToNull() throws IOException, InvalidSep10ChallengeException { KeyPair server = KeyPair.random(); KeyPair client = KeyPair.random(); + Network network = Network.TESTNET; String domainName = "example.com"; + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + Transaction transaction = Sep10Challenge.newChallenge( + server, + network, + client.getAccountId(), + domainName, + timeBounds + ); + + Sep10Challenge.ChallengeTransaction challengeTransaction = Sep10Challenge.readChallengeTransaction(transaction.toEnvelopeXdrBase64(), server.getAccountId(), Network.TESTNET, null); + assertEquals(new Sep10Challenge.ChallengeTransaction(transaction, client.getAccountId()), challengeTransaction); + } + + @Test + public void testReadChallengeTransactionValidDoesNotVerifyHomeDomainWithHomeDomainSetToInvalidValue() throws IOException, InvalidSep10ChallengeException { + KeyPair server = KeyPair.random(); + KeyPair client = KeyPair.random(); Network network = Network.TESTNET; + String domainName = "example.com"; long now = System.currentTimeMillis() / 1000L; long end = now + 300; TimeBounds timeBounds = new TimeBounds(now, end); - byte[] encodedNonce = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?AAAAAAAAAAAAAAAAAAAAAAAAAA".getBytes("UTF-8"); + Transaction transaction = Sep10Challenge.newChallenge( + server, + network, + client.getAccountId(), + domainName, + timeBounds + ); + + Sep10Challenge.ChallengeTransaction challengeTransaction = Sep10Challenge.readChallengeTransaction(transaction.toEnvelopeXdrBase64(), server.getAccountId(), Network.TESTNET, "invalid.domain"); + assertEquals(new Sep10Challenge.ChallengeTransaction(transaction, client.getAccountId()), challengeTransaction); + } + + @Test + public void testReadChallengeTransactionValidAdditionalManageDataOpsWithSourceAccountSetToServerAccount() throws IOException, InvalidSep10ChallengeException { + KeyPair server = KeyPair.random(); + KeyPair client = KeyPair.random(); + String domainName = "example.com"; + + Network network = Network.TESTNET; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + byte[] nonce = new byte[48]; + SecureRandom random = new SecureRandom(); + random.nextBytes(nonce); + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); + Account sourceAccount = new Account(server.getAccountId(), -1L); - ManageDataOperation manageDataOperation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + ManageDataOperation operation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) .setSourceAccount(client.getAccountId()) .build(); + ManageDataOperation operation2 = new ManageDataOperation.Builder("key", "value".getBytes()) + .setSourceAccount(server.getAccountId()) + .build(); + Operation[] operations = new Operation[]{operation1, operation2}; + Transaction transaction = new Transaction( + sourceAccount.getAccountId(), + 100 * operations.length, + sourceAccount.getIncrementedSequenceNumber(), + operations, + Memo.none(), + timeBounds, + network + ); + transaction.sign(server); + String challenge = transaction.toEnvelopeXdrBase64(); - Operation[] operations = new Operation[]{manageDataOperation1}; + Sep10Challenge.ChallengeTransaction challengeTransaction = Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); + assertEquals(new Sep10Challenge.ChallengeTransaction(transaction, client.getAccountId()), challengeTransaction); + } + + @Test + public void testReadChallengeTransactionInvalidAdditionalManageDataOpsWithoutSourceAccountSetToServerAccount() throws IOException { + KeyPair server = KeyPair.random(); + KeyPair client = KeyPair.random(); + String domainName = "example.com"; + + Network network = Network.TESTNET; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + byte[] nonce = new byte[48]; + SecureRandom random = new SecureRandom(); + random.nextBytes(nonce); + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); + + Account sourceAccount = new Account(server.getAccountId(), -1L); + ManageDataOperation operation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + .setSourceAccount(client.getAccountId()) + .build(); + ManageDataOperation operation2 = new ManageDataOperation.Builder("key", "value".getBytes()) + .setSourceAccount(client.getAccountId()) + .build(); + Operation[] operations = new Operation[]{operation1, operation2}; Transaction transaction = new Transaction( sourceAccount.getAccountId(), 100 * operations.length, @@ -742,13 +874,12 @@ public void testReadChallengeTransactionInvalidDataValueCorruptBase64() throws I Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); fail(); } catch (InvalidSep10ChallengeException e) { - assertEquals("Failed to decode random nonce provided in ManageData operation.", e.getMessage()); - assertEquals("com.google.common.io.BaseEncoding$DecodingException: Unrecognized character: ?", e.getCause().getMessage()); + assertEquals("Subsequent operations are unrecognized.", e.getMessage()); } } @Test - public void testReadChallengeTransactionInvalidDataValueWrongByteLength() throws IOException { + public void testReadChallengeTransactionInvalidAdditionalManageDataOpsWithSourceAccountSetToNull() throws IOException { KeyPair server = KeyPair.random(); KeyPair client = KeyPair.random(); String domainName = "example.com"; @@ -759,18 +890,18 @@ public void testReadChallengeTransactionInvalidDataValueWrongByteLength() throws long end = now + 300; TimeBounds timeBounds = new TimeBounds(now, end); - byte[] nonce = new byte[47]; + byte[] nonce = new byte[48]; SecureRandom random = new SecureRandom(); random.nextBytes(nonce); BaseEncoding base64Encoding = BaseEncoding.base64(); byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); Account sourceAccount = new Account(server.getAccountId(), -1L); - ManageDataOperation manageDataOperation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + ManageDataOperation operation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) .setSourceAccount(client.getAccountId()) .build(); - - Operation[] operations = new Operation[]{manageDataOperation1}; + ManageDataOperation operation2 = new ManageDataOperation.Builder("key", "value".getBytes()).build(); + Operation[] operations = new Operation[]{operation1, operation2}; Transaction transaction = new Transaction( sourceAccount.getAccountId(), 100 * operations.length, @@ -787,40 +918,53 @@ public void testReadChallengeTransactionInvalidDataValueWrongByteLength() throws Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); fail(); } catch (InvalidSep10ChallengeException e) { - assertEquals("Random nonce before encoding as base64 should be 48 bytes long.", e.getMessage()); + assertEquals("Operation should have a source account.", e.getMessage()); } } @Test - public void testReadChallengeTransactionInvalidDomainNameMismatch() throws IOException { + public void testReadChallengeTransactionInvalidAdditionalOpsOfOtherTypes() throws IOException { KeyPair server = KeyPair.random(); KeyPair client = KeyPair.random(); - Network network = Network.TESTNET; String domainName = "example.com"; - String mismatchDomainName = "mismatch_example.com"; + + Network network = Network.TESTNET; long now = System.currentTimeMillis() / 1000L; long end = now + 300; TimeBounds timeBounds = new TimeBounds(now, end); - Transaction transaction = null; - try { - transaction = Sep10Challenge.newChallenge( - server, - network, - client.getAccountId(), - domainName, - timeBounds - ); - } catch (InvalidSep10ChallengeException e) { - fail("Should not have thrown any exception."); - } + byte[] nonce = new byte[48]; + SecureRandom random = new SecureRandom(); + random.nextBytes(nonce); + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); + + Account sourceAccount = new Account(server.getAccountId(), -1L); + ManageDataOperation operation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + .setSourceAccount(client.getAccountId()) + .build(); + BumpSequenceOperation operation2 = new BumpSequenceOperation.Builder(0L) + .setSourceAccount(server.getAccountId()) + .build(); + Operation[] operations = new Operation[]{operation1, operation2}; + Transaction transaction = new Transaction( + sourceAccount.getAccountId(), + 100 * operations.length, + sourceAccount.getIncrementedSequenceNumber(), + operations, + Memo.none(), + timeBounds, + network + ); + transaction.sign(server); + String challenge = transaction.toEnvelopeXdrBase64(); try { - Sep10Challenge.readChallengeTransaction(transaction.toEnvelopeXdrBase64(), server.getAccountId(), Network.TESTNET, mismatchDomainName); + Sep10Challenge.readChallengeTransaction(challenge, server.getAccountId(), Network.TESTNET, domainName); fail(); } catch (InvalidSep10ChallengeException e) { - assertEquals("The transaction's operation key name does not include the expected home domain.", e.getMessage()); + assertEquals("Operation type should be ManageData.", e.getMessage()); } } @@ -1645,4 +1789,236 @@ public void testVerifyChallengeTransactionSignersInvalidNoSignersEmptySet() thro assertEquals("No verifiable signers provided, at least one G... address must be provided.", e.getMessage()); } } + + @Test + public void testVerifyChallengeTransactionValidDoesNotVerifyHomeDomainHomeDomainSetToNull() throws IOException, InvalidSep10ChallengeException { + KeyPair server = KeyPair.random(); + KeyPair masterClient = KeyPair.random(); + Network network = Network.TESTNET; + String domainName = "example.com"; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + Transaction transaction = Sep10Challenge.newChallenge( + server, + network, + masterClient.getAccountId(), + domainName, + timeBounds + ); + transaction.sign(masterClient); + + Set signers = new HashSet(Collections.singletonList(masterClient.getAccountId())); + Set signersFound = Sep10Challenge.verifyChallengeTransactionSigners(transaction.toEnvelopeXdrBase64(), server.getAccountId(), network, null, signers); + assertEquals(signers, signersFound); + } + + @Test + public void testVerifyChallengeTransactionValidDoesNotVerifyHomeDomainWithHomeDomainSetToInvalidValue() throws IOException, InvalidSep10ChallengeException { + KeyPair server = KeyPair.random(); + KeyPair masterClient = KeyPair.random(); + Network network = Network.TESTNET; + String domainName = "example.com"; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + Transaction transaction = Sep10Challenge.newChallenge( + server, + network, + masterClient.getAccountId(), + domainName, + timeBounds + ); + transaction.sign(masterClient); + + Set signers = new HashSet(Collections.singletonList(masterClient.getAccountId())); + Set signersFound = Sep10Challenge.verifyChallengeTransactionSigners(transaction.toEnvelopeXdrBase64(), server.getAccountId(), network, "invalid.domain", signers); + assertEquals(signers, signersFound); + } + + @Test + public void testVerifyChallengeTransactionValidAdditionalManageDataOpsWithSourceAccountSetToServerAccount() throws IOException, InvalidSep10ChallengeException { + KeyPair server = KeyPair.random(); + KeyPair masterClient = KeyPair.random(); + String domainName = "example.com"; + + Network network = Network.TESTNET; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + byte[] nonce = new byte[48]; + SecureRandom random = new SecureRandom(); + random.nextBytes(nonce); + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); + + Account sourceAccount = new Account(server.getAccountId(), -1L); + ManageDataOperation operation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + .setSourceAccount(masterClient.getAccountId()) + .build(); + ManageDataOperation operation2 = new ManageDataOperation.Builder("key", "value".getBytes()) + .setSourceAccount(server.getAccountId()) + .build(); + Operation[] operations = new Operation[]{operation1, operation2}; + Transaction transaction = new Transaction( + sourceAccount.getAccountId(), + 100 * operations.length, + sourceAccount.getIncrementedSequenceNumber(), + operations, + Memo.none(), + timeBounds, + network + ); + transaction.sign(server); + transaction.sign(masterClient); + + Set signers = new HashSet(Collections.singletonList(masterClient.getAccountId())); + Set signersFound = Sep10Challenge.verifyChallengeTransactionSigners(transaction.toEnvelopeXdrBase64(), server.getAccountId(), network, domainName, signers); + assertEquals(signers, signersFound); + } + + @Test + public void testVerifyChallengeTransactionInvalidAdditionalManageDataOpsWithoutSourceAccountSetToServerAccount() throws IOException { + KeyPair server = KeyPair.random(); + KeyPair masterClient = KeyPair.random(); + String domainName = "example.com"; + + Network network = Network.TESTNET; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + byte[] nonce = new byte[48]; + SecureRandom random = new SecureRandom(); + random.nextBytes(nonce); + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); + + Account sourceAccount = new Account(server.getAccountId(), -1L); + ManageDataOperation operation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + .setSourceAccount(masterClient.getAccountId()) + .build(); + ManageDataOperation operation2 = new ManageDataOperation.Builder("key", "value".getBytes()) + .setSourceAccount(masterClient.getAccountId()) + .build(); + Operation[] operations = new Operation[]{operation1, operation2}; + Transaction transaction = new Transaction( + sourceAccount.getAccountId(), + 100 * operations.length, + sourceAccount.getIncrementedSequenceNumber(), + operations, + Memo.none(), + timeBounds, + network + ); + transaction.sign(server); + transaction.sign(masterClient); + + Set signers = new HashSet(Collections.singletonList(masterClient.getAccountId())); + try { + Sep10Challenge.verifyChallengeTransactionSigners(transaction.toEnvelopeXdrBase64(), server.getAccountId(), network, domainName, signers); + fail(); + } catch (InvalidSep10ChallengeException e) { + assertEquals("Subsequent operations are unrecognized.", e.getMessage()); + } + } + + @Test + public void testVerifyChallengeTransactionInvalidAdditionalManageDataOpsWithSourceAccountSetToNull() throws IOException { + KeyPair server = KeyPair.random(); + KeyPair masterClient = KeyPair.random(); + String domainName = "example.com"; + + Network network = Network.TESTNET; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + byte[] nonce = new byte[48]; + SecureRandom random = new SecureRandom(); + random.nextBytes(nonce); + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); + + Account sourceAccount = new Account(server.getAccountId(), -1L); + ManageDataOperation operation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + .setSourceAccount(masterClient.getAccountId()) + .build(); + ManageDataOperation operation2 = new ManageDataOperation.Builder("key", "value".getBytes()).build(); + Operation[] operations = new Operation[]{operation1, operation2}; + Transaction transaction = new Transaction( + sourceAccount.getAccountId(), + 100 * operations.length, + sourceAccount.getIncrementedSequenceNumber(), + operations, + Memo.none(), + timeBounds, + network + ); + transaction.sign(server); + transaction.sign(masterClient); + + Set signers = new HashSet(Collections.singletonList(masterClient.getAccountId())); + try { + Sep10Challenge.verifyChallengeTransactionSigners(transaction.toEnvelopeXdrBase64(), server.getAccountId(), network, domainName, signers); + fail(); + } catch (InvalidSep10ChallengeException e) { + assertEquals("Operation should have a source account.", e.getMessage()); + } + } + + @Test + public void testVerifyChallengeTransactionInvalidAdditionalOpsOfOtherTypes() throws IOException { + KeyPair server = KeyPair.random(); + KeyPair masterClient = KeyPair.random(); + String domainName = "example.com"; + + Network network = Network.TESTNET; + + long now = System.currentTimeMillis() / 1000L; + long end = now + 300; + TimeBounds timeBounds = new TimeBounds(now, end); + + byte[] nonce = new byte[48]; + SecureRandom random = new SecureRandom(); + random.nextBytes(nonce); + BaseEncoding base64Encoding = BaseEncoding.base64(); + byte[] encodedNonce = base64Encoding.encode(nonce).getBytes(); + + Account sourceAccount = new Account(server.getAccountId(), -1L); + ManageDataOperation operation1 = new ManageDataOperation.Builder(domainName + " auth", encodedNonce) + .setSourceAccount(masterClient.getAccountId()) + .build(); + BumpSequenceOperation operation2 = new BumpSequenceOperation.Builder(0L) + .setSourceAccount(server.getAccountId()) + .build(); + Operation[] operations = new Operation[]{operation1, operation2}; + Transaction transaction = new Transaction( + sourceAccount.getAccountId(), + 100 * operations.length, + sourceAccount.getIncrementedSequenceNumber(), + operations, + Memo.none(), + timeBounds, + network + ); + transaction.sign(server); + transaction.sign(masterClient); + + Set signers = new HashSet(Collections.singletonList(masterClient.getAccountId())); + try { + Sep10Challenge.verifyChallengeTransactionSigners(transaction.toEnvelopeXdrBase64(), server.getAccountId(), network, domainName, signers); + fail(); + } catch (InvalidSep10ChallengeException e) { + assertEquals("Operation type should be ManageData.", e.getMessage()); + } + } }