diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java index 39dcb24a4678430b686f1f56b374cedb982f6127..9070c738f5e934e97b0533ddaa7f9fd112e70aed 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java @@ -124,6 +124,9 @@ abstract class ConversationItem { text = ctx.getString( R.string.introduction_response_accepted_sent, ir.getName()); + text += "\n\n" + ctx.getString( + R.string.introduction_response_accepted_sent_info, + ir.getName()); } else { text = ctx.getString( R.string.introduction_response_declined_sent, diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 38b04cd534ba1ac46342e839d5f61b8e7d90514b..d77582999d08210d1ab55c8a1a10c0dc8b8bb51c 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -156,6 +156,7 @@ <string name="introduction_request_exists_received">%1$s has asked to introduce you to %2$s, but %2$s is already in your contact list. Since %1$s might not know that, you can still respond:</string> <string name="introduction_request_answered_received">%1$s has asked to introduce you to %2$s.</string> <string name="introduction_response_accepted_sent">You accepted the introduction to %1$s.</string> + <string name="introduction_response_accepted_sent_info">Before %1$s gets added to your contacts, they need to accept the introduction as well. This might take some time.</string> <string name="introduction_response_declined_sent">You declined the introduction to %1$s.</string> <string name="introduction_response_accepted_received">%1$s accepted the introduction to %2$s.</string> <string name="introduction_response_declined_received">%1$s declined the introduction to %2$s.</string> diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java index 8711193f15b9e4696fa0bebb8bb2d8f61d9aeb16..9a267c5c205304087f62c4add918d73bd1613c2c 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java @@ -25,6 +25,9 @@ public interface IntroductionManager extends ConversationClient { */ int CLIENT_VERSION = 1; + /** + * Returns true if both contacts can be introduced at this moment. + */ boolean canIntroduce(Contact c1, Contact c2) throws DbException; /** diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java index ae395b596928734020c2fa89c9afcaaad62174b1..03b3486ada568e39f8ad457fb2b6c45159dff2ea 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java @@ -144,15 +144,15 @@ abstract class AbstractProtocolEngine<S extends Session> } } - void broadcastIntroductionResponseReceivedEvent(Transaction txn, - Session s, AuthorId sender, AbstractIntroductionMessage m) + void broadcastIntroductionResponseReceivedEvent(Transaction txn, Session s, + AuthorId sender, Author otherAuthor, AbstractIntroductionMessage m) throws DbException { AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId(); Contact c = contactManager.getContact(txn, sender, localAuthorId); IntroductionResponse response = new IntroductionResponse(s.getSessionId(), m.getMessageId(), m.getGroupId(), s.getRole(), m.getTimestamp(), false, - false, false, false, c.getAuthor().getName(), + false, false, false, otherAuthor.getName(), m instanceof AcceptMessage); IntroductionResponseReceivedEvent e = new IntroductionResponseReceivedEvent(c.getId(), response); diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java index 9f7f83ab644627a00f20de8f7cff06013ba08b4a..e2ea32ed62209bfbf7dc93bd65deb6078fb1e3a1 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java @@ -355,7 +355,7 @@ class IntroduceeProtocolEngine // Broadcast IntroductionResponseReceivedEvent broadcastIntroductionResponseReceivedEvent(txn, s, - s.getIntroducer().getId(), m); + s.getIntroducer().getId(), s.getRemote().author, m); if (s.getState() == AWAIT_RESPONSES) { // Mark the request message unavailable to answer diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java index 50b9d01a949723be57fbc9baa7b64916e0762e6a..a346a0bf32129320a99861377f7a90cb8159b4f1 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java @@ -261,19 +261,24 @@ class IntroducerProtocolEngine // Create the next state IntroducerState state = AWAIT_AUTHS; Introducee introduceeA, introduceeB; + Author sender, other; if (senderIsAlice) { if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_B; introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); introduceeB = new Introducee(s.getIntroduceeB(), sent); + sender = introduceeA.author; + other = introduceeB.author; } else { if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_A; introduceeA = new Introducee(s.getIntroduceeA(), sent); introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + sender = introduceeB.author; + other = introduceeA.author; } // Broadcast IntroductionResponseReceivedEvent - Author sender = senderIsAlice ? introduceeA.author : introduceeB.author; - broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m); + broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), + other, m); // Move to the next state return new IntroducerSession(s.getSessionId(), state, @@ -313,17 +318,22 @@ class IntroducerProtocolEngine m.getTransportProperties(), false); Introducee introduceeA, introduceeB; + Author sender, other; if (senderIsAlice) { introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); introduceeB = new Introducee(s.getIntroduceeB(), sent); + sender = introduceeA.author; + other = introduceeB.author; } else { introduceeA = new Introducee(s.getIntroduceeA(), sent); introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + sender = introduceeB.author; + other = introduceeA.author; } // Broadcast IntroductionResponseReceivedEvent - Author sender = senderIsAlice ? introduceeA.author : introduceeB.author; - broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m); + broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), + other, m); return new IntroducerSession(s.getSessionId(), START, s.getRequestTimestamp(), introduceeA, introduceeB); @@ -360,19 +370,24 @@ class IntroducerProtocolEngine // Create the next state IntroducerState state = START; Introducee introduceeA, introduceeB; + Author sender, other; if (senderIsAlice) { if (s.getState() == AWAIT_RESPONSES) state = A_DECLINED; introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); introduceeB = new Introducee(s.getIntroduceeB(), sent); + sender = introduceeA.author; + other = introduceeB.author; } else { if (s.getState() == AWAIT_RESPONSES) state = B_DECLINED; introduceeA = new Introducee(s.getIntroduceeA(), sent); introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + sender = introduceeB.author; + other = introduceeA.author; } // Broadcast IntroductionResponseReceivedEvent - Author sender = senderIsAlice ? introduceeA.author : introduceeB.author; - broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m); + broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), + other, m); return new IntroducerSession(s.getSessionId(), state, s.getRequestTimestamp(), introduceeA, introduceeB); @@ -405,17 +420,22 @@ class IntroducerProtocolEngine Message sent = sendDeclineMessage(txn, i, timestamp, false); Introducee introduceeA, introduceeB; + Author sender, other; if (senderIsAlice) { introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); introduceeB = new Introducee(s.getIntroduceeB(), sent); + sender = introduceeA.author; + other = introduceeB.author; } else { introduceeA = new Introducee(s.getIntroduceeA(), sent); introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + sender = introduceeB.author; + other = introduceeA.author; } // Broadcast IntroductionResponseReceivedEvent - Author sender = senderIsAlice ? introduceeA.author : introduceeB.author; - broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m); + broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), + other, m); return new IntroducerSession(s.getSessionId(), START, s.getRequestTimestamp(), introduceeA, introduceeB); diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java index df0d46b8811e60e6f8f58919ef388a590fd4363e..b8e5b646bccb84c37dae41f385c5102200f449f6 100644 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java @@ -28,6 +28,7 @@ import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.introduction.IntroductionMessage; import org.briarproject.briar.api.introduction.IntroductionRequest; +import org.briarproject.briar.api.introduction.IntroductionResponse; import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent; import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent; import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent; @@ -51,6 +52,7 @@ import static org.briarproject.bramble.test.TestUtils.getTransportProperties; import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap; import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION; +import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSES; import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_DECLINED; import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED; import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED; @@ -146,24 +148,32 @@ public class IntroductionIntegrationTest sync0To1(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); + assertEquals(introducee2.getAuthor().getName(), + listener1.getRequest().getName()); assertGroupCount(messageTracker1, g1.getId(), 2, 1); // sync second REQUEST message sync0To2(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener2.requestReceived); + assertEquals(introducee1.getAuthor().getName(), + listener2.getRequest().getName()); assertGroupCount(messageTracker2, g2.getId(), 2, 1); // sync first ACCEPT message sync1To0(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.response1Received); + assertEquals(introducee2.getAuthor().getName(), + listener0.getResponse().getName()); assertGroupCount(messageTracker0, g1.getId(), 2, 1); // sync second ACCEPT message sync2To0(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.response2Received); + assertEquals(introducee1.getAuthor().getName(), + listener0.getResponse().getName()); assertGroupCount(messageTracker0, g2.getId(), 2, 1); // sync forwarded ACCEPT messages to introducees @@ -259,6 +269,10 @@ public class IntroductionIntegrationTest assertEquals(alice ? A_DECLINED : B_DECLINED, introducerSession.getState()); + // assert that the name on the decline event is correct + assertEquals(introducee2.getAuthor().getName(), + listener0.getResponse().getName()); + // sync second response sync2To0(1, true); eventWaiter.await(TIMEOUT, 1); @@ -271,6 +285,11 @@ public class IntroductionIntegrationTest // sync first forwarded response sync0To2(1, true); + // assert that the name on the decline event is correct + eventWaiter.await(TIMEOUT, 1); + assertEquals(introducee1.getAuthor().getName(), + listener2.getResponse().getName()); + // note how the introducer does not forward the second response, // because after the first decline the protocol finished @@ -339,6 +358,11 @@ public class IntroductionIntegrationTest sync0To2(1, true); sync0To1(1, true); + // assert that the name on the decline event is correct + eventWaiter.await(TIMEOUT, 1); + assertEquals(contact2From0.getAuthor().getName(), + listener1.getResponse().getName()); + assertFalse(contactManager1 .contactExists(author2.getId(), author1.getId())); assertFalse(contactManager2 @@ -408,8 +432,6 @@ public class IntroductionIntegrationTest assertFalse(contactManager2 .contactExists(author1.getId(), author2.getId())); - // since introducee2 was already in FINISHED state when - // introducee1's response arrived, she ignores and deletes it assertDefaultUiMessages(); assertFalse(listener0.aborted); assertFalse(listener1.aborted); @@ -417,7 +439,7 @@ public class IntroductionIntegrationTest } @Test - public void testResponseAndAckInOneSession() throws Exception { + public void testResponseAndAuthInOneSync() throws Exception { addListeners(true, true); // make introduction @@ -449,10 +471,125 @@ public class IntroductionIntegrationTest .respondToIntroduction(contactId0From2, listener2.sessionId, time, true); - // sync second response and ACK and make sure there is no abort + // sync second response and AUTH sync2To0(2, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.response2Received); + + // Forward AUTH + sync0To1(1, true); + + // Second AUTH and ACTIATE and forward them + sync1To0(2, true); + sync0To2(2, true); + + assertTrue(contactManager1 + .contactExists(author2.getId(), author1.getId())); + assertTrue(contactManager2 + .contactExists(author1.getId(), author2.getId())); + + assertDefaultUiMessages(); + assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertFalse(listener2.aborted); + } + + /** + * When an introducee declines an introduction, + * the other introducee needs to respond before returning to START state, + * otherwise a subsequent attempt at introducing the same contacts will fail + */ + @Test + public void testAutomaticSecondDecline() throws Exception { + // introducee1 declines automatically and introducee2 doesn't answer + addListeners(false, true); + listener2.answerRequests = false; + + // make introduction + long time = clock.currentTimeMillis(); + Contact introducee1 = contact1From0; + Contact introducee2 = contact2From0; + introductionManager0 + .makeIntroduction(introducee1, introducee2, null, time); + + // sync request messages + sync0To1(1, true); + sync0To2(1, true); + + // assert that introducee1 is in correct state + IntroduceeSession introduceeSession = getIntroduceeSession(c1); + assertEquals(LOCAL_DECLINED, introduceeSession.getState()); + + // sync first response + sync1To0(1, true); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.response1Received); + + // assert that introducer is in correct state + boolean alice = c0.getIntroductionCrypto() + .isAlice(introducee1.getAuthor().getId(), + introducee2.getAuthor().getId()); + IntroducerSession introducerSession = getIntroducerSession(); + assertEquals(alice ? A_DECLINED : B_DECLINED, + introducerSession.getState()); + + // assert that introducee2 is in correct state + introduceeSession = getIntroduceeSession(c2); + assertEquals(AWAIT_RESPONSES, introduceeSession.getState()); + + // forward first DECLINE + sync0To2(1, true); + + // assert that the name on the decline event is correct + eventWaiter.await(TIMEOUT, 1); + assertEquals(introducee1.getAuthor().getName(), + listener2.getResponse().getName()); + + // assert that introducee2 is in correct state + introduceeSession = getIntroduceeSession(c2); + assertEquals(IntroduceeState.START, introduceeSession.getState()); + + // second response should be an immediate automatic DECLINE + sync2To0(1, true); + eventWaiter.await(TIMEOUT, 1); + assertTrue(listener0.response2Received); + + // assert that introducer now moved to START state + introducerSession = getIntroducerSession(); + assertEquals(START, introducerSession.getState()); + + // introducee1 is still waiting for second response + introduceeSession = getIntroduceeSession(c1); + assertEquals(LOCAL_DECLINED, introduceeSession.getState()); + + // forward automatic decline + sync0To1(1, true); + + // introducee1 can finally move to the START + introduceeSession = getIntroduceeSession(c1); + assertEquals(IntroduceeState.START, introduceeSession.getState()); + + Group g1 = introductionManager0.getContactGroup(introducee1); + Group g2 = introductionManager0.getContactGroup(introducee2); + assertEquals(2, + introductionManager0.getIntroductionMessages(contactId1From0) + .size()); + assertGroupCount(messageTracker0, g1.getId(), 2, 1); + assertEquals(2, + introductionManager0.getIntroductionMessages(contactId2From0) + .size()); + assertGroupCount(messageTracker0, g2.getId(), 2, 1); + assertEquals(2, + introductionManager1.getIntroductionMessages(contactId0From1) + .size()); + assertGroupCount(messageTracker1, g1.getId(), 2, 1); + // the automatic DECLINE is invisible in the UI + // so there's just the remote REQUEST and remote DECLINE + assertEquals(2, + introductionManager2.getIntroductionMessages(contactId0From2) + .size()); + assertGroupCount(messageTracker2, g2.getId(), 2, 2); + assertFalse(listener0.aborted); assertFalse(listener1.aborted); assertFalse(listener2.aborted); @@ -1012,11 +1149,25 @@ public class IntroductionIntegrationTest @MethodsNotNullByDefault @ParametersNotNullByDefault - private class IntroduceeListener implements EventListener { + private abstract class IntroductionListener implements EventListener { + + protected volatile boolean aborted = false; + protected volatile Event latestEvent; + + IntroductionResponse getResponse() { + assertTrue( + latestEvent instanceof IntroductionResponseReceivedEvent); + return ((IntroductionResponseReceivedEvent) latestEvent) + .getIntroductionResponse(); + } + } + + @MethodsNotNullByDefault + @ParametersNotNullByDefault + private class IntroduceeListener extends IntroductionListener { private volatile boolean requestReceived = false; private volatile boolean succeeded = false; - private volatile boolean aborted = false; private volatile boolean answerRequests = true; private volatile SessionId sessionId; @@ -1031,6 +1182,7 @@ public class IntroductionIntegrationTest @Override public void eventOccurred(Event e) { if (e instanceof IntroductionRequestReceivedEvent) { + latestEvent = e; IntroductionRequestReceivedEvent introEvent = ((IntroductionRequestReceivedEvent) e); requestReceived = true; @@ -1053,29 +1205,42 @@ public class IntroductionIntegrationTest } finally { eventWaiter.resume(); } + } else if (e instanceof IntroductionResponseReceivedEvent) { + // only broadcast for DECLINE messages in introducee role + latestEvent = e; + eventWaiter.resume(); } else if (e instanceof IntroductionSucceededEvent) { + latestEvent = e; succeeded = true; Contact contact = ((IntroductionSucceededEvent) e).getContact(); eventWaiter .assertFalse(contact.getId().equals(contactId0From1)); eventWaiter.resume(); } else if (e instanceof IntroductionAbortedEvent) { + latestEvent = e; aborted = true; eventWaiter.resume(); } } + + private IntroductionRequest getRequest() { + assertTrue( + latestEvent instanceof IntroductionRequestReceivedEvent); + return ((IntroductionRequestReceivedEvent) latestEvent) + .getIntroductionRequest(); + } } @NotNullByDefault - private class IntroducerListener implements EventListener { + private class IntroducerListener extends IntroductionListener { private volatile boolean response1Received = false; private volatile boolean response2Received = false; - private volatile boolean aborted = false; @Override public void eventOccurred(Event e) { if (e instanceof IntroductionResponseReceivedEvent) { + latestEvent = e; ContactId c = ((IntroductionResponseReceivedEvent) e) .getContactId(); @@ -1086,6 +1251,7 @@ public class IntroductionIntegrationTest } eventWaiter.resume(); } else if (e instanceof IntroductionAbortedEvent) { + latestEvent = e; aborted = true; eventWaiter.resume(); }