diff --git a/.idea/dictionaries/briar.xml b/.idea/dictionaries/briar.xml index 71bcd22b4..7eb591723 100644 --- a/.idea/dictionaries/briar.xml +++ b/.idea/dictionaries/briar.xml @@ -7,6 +7,7 @@ encrypter identicon introducee + introducees introducer onboarding diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java index 53e1cc76d..061a57ccb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java @@ -289,6 +289,10 @@ class ConversationVisitor implements text = ctx.getString( R.string.introduction_response_accepted_sent, introducedAuthor) + suffix; + } else if (r.isAutoDecline()) { + text = ctx.getString( + R.string.introduction_response_declined_auto, + introducedAuthor); } 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 26cfa3011..edce11614 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -317,6 +317,7 @@ You accepted the introduction to %1$s. Before %1$s gets added to your contacts, they need to accept the introduction as well. This might take some time. You declined the introduction to %1$s. + The introduction to %1$s was automatically declined. %1$s accepted the introduction to %2$s. %1$s declined the introduction to %2$s. %1$s says that %2$s declined the introduction. diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java index d05baadc0..716432e3c 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java @@ -26,9 +26,9 @@ public class IntroductionResponse extends ConversationResponse { boolean local, boolean read, boolean sent, boolean seen, SessionId sessionId, boolean accepted, Author author, AuthorInfo introducedAuthorInfo, Role role, boolean canSucceed, - long autoDeleteTimer) { + long autoDeleteTimer, boolean isAutoDecline) { super(messageId, groupId, time, local, read, sent, seen, sessionId, - accepted, autoDeleteTimer, false); + accepted, autoDeleteTimer, isAutoDecline); this.introducedAuthor = author; this.introducedAuthorInfo = introducedAuthorInfo; this.ourRole = role; 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 7d1894a3a..4760b25a4 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 @@ -11,6 +11,7 @@ import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.IdentityManager; @@ -39,6 +40,7 @@ import javax.annotation.concurrent.Immutable; import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION; +import static org.briarproject.briar.api.introduction.Role.INTRODUCEE; import static org.briarproject.briar.introduction.MessageType.ABORT; import static org.briarproject.briar.introduction.MessageType.ACCEPT; import static org.briarproject.briar.introduction.MessageType.ACTIVATE; @@ -105,6 +107,10 @@ abstract class AbstractProtocolEngine> m = messageEncoder.encodeRequestMessage(s.getContactGroupId(), timestamp, s.getLastLocalMessageId(), author, text, timer); sendMessage(txn, REQUEST, s.getSessionId(), m, true, timer); + // Set the auto-delete timer duration on the local message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } } else { m = messageEncoder.encodeRequestMessage(s.getContactGroupId(), timestamp, s.getLastLocalMessageId(), author, text); @@ -128,6 +134,10 @@ abstract class AbstractProtocolEngine> ephemeralPublicKey, acceptTimestamp, transportProperties, timer); sendMessage(txn, ACCEPT, s.getSessionId(), m, visible, timer); + // Set the auto-delete timer duration on the message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } } else { m = messageEncoder.encodeAcceptMessage(s.getContactGroupId(), timestamp, s.getLastLocalMessageId(), s.getSessionId(), @@ -139,7 +149,8 @@ abstract class AbstractProtocolEngine> } Message sendDeclineMessage(Transaction txn, PeerSession s, long timestamp, - boolean visible) throws DbException { + boolean visible, boolean isAutoDecline) throws DbException { + if (!visible && isAutoDecline) throw new IllegalArgumentException(); Message m; ContactId c = getContactId(txn, s.getContactGroupId()); if (contactSupportsAutoDeletion(txn, c)) { @@ -148,7 +159,25 @@ abstract class AbstractProtocolEngine> m = messageEncoder.encodeDeclineMessage(s.getContactGroupId(), timestamp, s.getLastLocalMessageId(), s.getSessionId(), timer); - sendMessage(txn, DECLINE, s.getSessionId(), m, visible, timer); + sendMessage(txn, DECLINE, s.getSessionId(), m, visible, timer, + isAutoDecline); + // Set the auto-delete timer duration on the local message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } + if (isAutoDecline) { + // Broadcast an event, so the auto-decline becomes visible + IntroduceeSession session = (IntroduceeSession) s; + Author author = session.getRemote().author; + AuthorInfo authorInfo = + authorManager.getAuthorInfo(txn, author.getId()); + IntroductionResponse response = new IntroductionResponse( + m.getId(), s.getContactGroupId(), m.getTimestamp(), + true, true, false, false, s.getSessionId(), false, + author, authorInfo, INTRODUCEE, false, timer, true); + Event e = new IntroductionResponseReceivedEvent(response, c); + txn.attach(e); + } } else { m = messageEncoder.encodeDeclineMessage(s.getContactGroupId(), timestamp, s.getLastLocalMessageId(), s.getSessionId()); @@ -192,9 +221,16 @@ abstract class AbstractProtocolEngine> private void sendMessage(Transaction txn, MessageType type, SessionId sessionId, Message m, boolean visibleInConversation, long autoDeleteTimer) throws DbException { + sendMessage(txn, type, sessionId, m, visibleInConversation, + autoDeleteTimer, false); + } + + private void sendMessage(Transaction txn, MessageType type, + SessionId sessionId, Message m, boolean visibleInConversation, + long autoDeleteTimer, boolean isAutoDecline) throws DbException { BdfDictionary meta = messageEncoder.encodeMetadata(type, sessionId, m.getTimestamp(), true, true, visibleInConversation, - autoDeleteTimer); + autoDeleteTimer, isAutoDecline); try { clientHelper.addLocalMessage(txn, m, meta, true, false); } catch (FormatException e) { @@ -215,7 +251,7 @@ abstract class AbstractProtocolEngine> m.getTimestamp(), false, false, false, false, s.getSessionId(), m instanceof AcceptMessage, otherAuthor, otherAuthorInfo, s.getRole(), canSucceed, - m.getAutoDeleteTimer()); + m.getAutoDeleteTimer(), false); IntroductionResponseReceivedEvent e = new IntroductionResponseReceivedEvent(response, c.getId()); txn.attach(e); 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 9475f5bdf..7f55f1a2a 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 @@ -123,12 +123,13 @@ class IntroduceeProtocolEngine @Override public IntroduceeSession onDeclineAction(Transaction txn, - IntroduceeSession session) throws DbException { + IntroduceeSession session, boolean isAutoDecline) + throws DbException { switch (session.getState()) { case AWAIT_RESPONSES: case REMOTE_DECLINED: case REMOTE_ACCEPTED: - return onLocalDecline(txn, session); + return onLocalDecline(txn, session, isAutoDecline); case START: case LOCAL_DECLINED: case LOCAL_ACCEPTED: @@ -319,13 +320,14 @@ class IntroduceeProtocolEngine } private IntroduceeSession onLocalDecline(Transaction txn, - IntroduceeSession s) throws DbException { + IntroduceeSession s, boolean isAutoDecline) throws DbException { // Mark the request message unavailable to answer markRequestsUnavailableToAnswer(txn, s); // Send a DECLINE message long localTimestamp = getTimestampForVisibleMessage(txn, s); - Message sent = sendDeclineMessage(txn, s, localTimestamp, true); + Message sent = + sendDeclineMessage(txn, s, localTimestamp, true, isAutoDecline); // Track the message messageTracker.trackOutgoingMessage(txn, sent); 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 b0d1dbdd7..f300c2d04 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 @@ -98,7 +98,7 @@ class IntroducerProtocolEngine @Override public IntroducerSession onDeclineAction(Transaction txn, - IntroducerSession s) { + IntroducerSession s, boolean isAutoDecline) { throw new UnsupportedOperationException(); // Invalid in this role } @@ -387,7 +387,7 @@ class IntroducerProtocolEngine Introducee i = getOtherIntroducee(s, m.getGroupId()); // The forwarded message will be visible to the introducee long localTimestamp = getTimestampForVisibleMessage(txn, s, i); - Message sent = sendDeclineMessage(txn, i, localTimestamp, false); + Message sent = sendDeclineMessage(txn, i, localTimestamp, false, false); // Create the next state IntroducerState state = START; @@ -442,7 +442,7 @@ class IntroducerProtocolEngine Introducee i = getOtherIntroducee(s, m.getGroupId()); // The forwarded message will be visible to the introducee long localTimestamp = getTimestampForVisibleMessage(txn, s, i); - Message sent = sendDeclineMessage(txn, i, localTimestamp, false); + Message sent = sendDeclineMessage(txn, i, localTimestamp, false, false); Introducee introduceeA, introduceeB; Author sender, other; diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java index 72a5066d7..37e914d36 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java @@ -10,6 +10,7 @@ interface IntroductionConstants { String MSG_KEY_VISIBLE_IN_UI = "visibleInUi"; String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer"; String MSG_KEY_AUTO_DELETE_TIMER = "autoDeleteTimer"; + String MSG_KEY_IS_AUTO_DECLINE = "isAutoDecline"; // Session Keys String SESSION_KEY_SESSION_ID = "sessionId"; diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java index df3a33d3b..6d3139ec8 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java @@ -20,7 +20,7 @@ interface IntroductionCrypto { /** * Returns true if the local author is alice - * + *

* Alice is the Author whose unique ID has the lower ID, * comparing the IDs as byte strings. */ diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java index 8d8a68a7a..d52192b49 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java @@ -1,6 +1,7 @@ package org.briarproject.briar.introduction; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.cleanup.CleanupHook; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ContactGroupFactory; import org.briarproject.bramble.api.contact.Contact; @@ -28,6 +29,7 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook; +import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.conversation.ConversationMessageHeader; @@ -54,8 +56,10 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; import static org.briarproject.briar.api.introduction.Role.INTRODUCEE; import static org.briarproject.briar.api.introduction.Role.INTRODUCER; +import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSES; import static org.briarproject.briar.introduction.IntroduceeState.REMOTE_DECLINED; import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED; import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED; @@ -71,7 +75,7 @@ import static org.briarproject.briar.introduction.MessageType.REQUEST; @NotNullByDefault class IntroductionManagerImpl extends ConversationClientImpl implements IntroductionManager, OpenDatabaseHook, ContactHook, - ClientVersioningHook { + ClientVersioningHook, CleanupHook { private final ClientVersioningManager clientVersioningManager; private final ContactGroupFactory contactGroupFactory; @@ -170,6 +174,11 @@ class IntroductionManagerImpl extends ConversationClientImpl BdfDictionary bdfMeta) throws DbException, FormatException { // Parse the metadata MessageMetadata meta = messageParser.parseMetadata(bdfMeta); + // set the clean-up timer that will be started when message gets read + long timer = meta.getAutoDeleteTimer(); + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } // Look up the session, if there is one SessionId sessionId = meta.getSessionId(); IntroduceeSession newIntroduceeSession = null; @@ -362,7 +371,19 @@ class IntroductionManagerImpl extends ConversationClientImpl @Override public void respondToIntroduction(ContactId contactId, SessionId sessionId, boolean accept) throws DbException { - Transaction txn = db.startTransaction(false); + respondToIntroduction(contactId, sessionId, accept, false); + } + + private void respondToIntroduction(ContactId contactId, SessionId sessionId, + boolean accept, boolean isAutoDecline) throws DbException { + db.transaction(false, + txn -> respondToIntroduction(txn, contactId, sessionId, accept, + isAutoDecline)); + } + + private void respondToIntroduction(Transaction txn, ContactId contactId, + SessionId sessionId, boolean accept, boolean isAutoDecline) + throws DbException { try { // Look up the session StoredSession ss = getSession(txn, sessionId); @@ -381,15 +402,13 @@ class IntroductionManagerImpl extends ConversationClientImpl if (accept) { session = introduceeEngine.onAcceptAction(txn, session); } else { - session = introduceeEngine.onDeclineAction(txn, session); + session = introduceeEngine + .onDeclineAction(txn, session, isAutoDecline); } // Store the updated session storeSession(txn, ss.storageId, session); - db.commitTransaction(txn); } catch (FormatException e) { throw new DbException(e); - } finally { - db.endTransaction(txn); } } @@ -487,7 +506,7 @@ class IntroductionManagerImpl extends ConversationClientImpl return new IntroductionResponse(m, contactGroupId, meta.getTimestamp(), meta.isLocal(), meta.isRead(), status.isSent(), status.isSeen(), sessionId, accept, author, authorInfo, role, canSucceed, - meta.getAutoDeleteTimer()); + meta.getAutoDeleteTimer(), meta.isAutoDecline()); } private void removeSessionWithIntroducer(Transaction txn, @@ -547,6 +566,67 @@ class IntroductionManagerImpl extends ConversationClientImpl } } + @Override + public void deleteMessages(Transaction txn, GroupId g, + Collection messageIds) throws DbException { + ContactId c; + Map sessions = new HashMap<>(); + try { + // get the ContactId from the given GroupId + c = clientHelper.getContactId(txn, g); + // get sessions for all messages to be deleted + for (MessageId messageId : messageIds) { + BdfDictionary d = clientHelper + .getMessageMetadataAsDictionary(txn, messageId); + MessageMetadata messageMetadata = + messageParser.parseMetadata(d); + if (!messageMetadata.isVisibleInConversation()) + throw new IllegalArgumentException(); + SessionId sessionId = messageMetadata.getSessionId(); + DeletableSession deletableSession = + sessions.get(sessionId); + if (deletableSession == null) { + StoredSession ss = getSession(txn, sessionId); + if (ss == null) throw new DbException(); + Role role = sessionParser.getRole(ss.bdfSession); + Session session; + if (role == INTRODUCER) { + session = sessionParser + .parseIntroducerSession(ss.bdfSession); + } else if (role == INTRODUCEE) { + session = sessionParser + .parseIntroduceeSession(g, ss.bdfSession); + } else throw new AssertionError(); + deletableSession = new DeletableSession(session.getState()); + sessions.put(sessionId, deletableSession); + } + deletableSession.messages.add(messageId); + } + } catch (FormatException e) { + throw new DbException(e); + } + + // delete given visible messages in sessions and auto-respond before + for (Entry entry : sessions.entrySet()) { + DeletableSession session = entry.getValue(); + // decline invitee sessions waiting for a response before + if (session.state instanceof IntroduceeState) { + IntroduceeState introduceeState = + (IntroduceeState) session.state; + if (introduceeState == AWAIT_RESPONSES) { + respondToIntroduction(txn, c, entry.getKey(), false, true); + } + } + for (MessageId m : session.messages) { + db.deleteMessage(txn, m); + db.deleteMessageMetadata(txn, m); + } + } + recalculateGroupCount(txn, g); + + txn.attach(new ConversationMessagesDeletedEvent(c, messageIds)); + } + @FunctionalInterface private interface MessageRetriever { /** diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java index 89d1f1fb7..e4529e9a6 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java @@ -1,5 +1,6 @@ package org.briarproject.briar.introduction; +import org.briarproject.bramble.api.cleanup.CleanupManager; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.data.MetadataEncoder; @@ -50,7 +51,8 @@ public class IntroductionModule { ValidationManager validationManager, ConversationManager conversationManager, ClientVersioningManager clientVersioningManager, - IntroductionManagerImpl introductionManager) { + IntroductionManagerImpl introductionManager, + CleanupManager cleanupManager) { lifecycleManager.registerOpenDatabaseHook(introductionManager); contactManager.registerContactHook(introductionManager); validationManager.registerIncomingMessageHook(CLIENT_ID, @@ -58,6 +60,8 @@ public class IntroductionModule { conversationManager.registerConversationClient(introductionManager); clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION, MINOR_VERSION, introductionManager); + cleanupManager.registerCleanupHook(CLIENT_ID, MAJOR_VERSION, + introductionManager); return introductionManager; } diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java index 604bee0d4..adaaa0aef 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java @@ -133,7 +133,7 @@ class IntroductionValidator extends BdfMessageValidator { SessionId sessionId = new SessionId(sessionIdBytes); BdfDictionary meta = messageEncoder.encodeMetadata(ACCEPT, sessionId, - m.getTimestamp(), false, false, false, timer); + m.getTimestamp(), timer); if (previousMessageId == null) { return new BdfMessageContext(meta); } else { @@ -163,7 +163,7 @@ class IntroductionValidator extends BdfMessageValidator { SessionId sessionId = new SessionId(sessionIdBytes); BdfDictionary meta = messageEncoder.encodeMetadata(type, sessionId, - m.getTimestamp(), false, false, false, timer); + m.getTimestamp(), timer); if (previousMessageId == null) { return new BdfMessageContext(meta); } else { @@ -190,7 +190,7 @@ class IntroductionValidator extends BdfMessageValidator { SessionId sessionId = new SessionId(sessionIdBytes); BdfDictionary meta = messageEncoder.encodeMetadata(AUTH, sessionId, - m.getTimestamp(), false, false, false, NO_AUTO_DELETE_TIMER); + m.getTimestamp(), NO_AUTO_DELETE_TIMER); MessageId dependency = new MessageId(previousMessageId); return new BdfMessageContext(meta, singletonList(dependency)); } @@ -210,7 +210,7 @@ class IntroductionValidator extends BdfMessageValidator { SessionId sessionId = new SessionId(sessionIdBytes); BdfDictionary meta = messageEncoder.encodeMetadata(ACTIVATE, sessionId, - m.getTimestamp(), false, false, false, NO_AUTO_DELETE_TIMER); + m.getTimestamp(), NO_AUTO_DELETE_TIMER); if (previousMessageId == null) { return new BdfMessageContext(meta); } else { @@ -231,7 +231,7 @@ class IntroductionValidator extends BdfMessageValidator { SessionId sessionId = new SessionId(sessionIdBytes); BdfDictionary meta = messageEncoder.encodeMetadata(type, sessionId, - m.getTimestamp(), false, false, false, NO_AUTO_DELETE_TIMER); + m.getTimestamp(), NO_AUTO_DELETE_TIMER); if (previousMessageId == null) { return new BdfMessageContext(meta); } else { diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java index 85fa4d4aa..516b3de2a 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java @@ -21,9 +21,14 @@ interface MessageEncoder { BdfDictionary encodeRequestMetadata(long timestamp, long autoDeleteTimer); + BdfDictionary encodeMetadata(MessageType type, + @Nullable SessionId sessionId, long timestamp, + long autoDeleteTimer); + BdfDictionary encodeMetadata(MessageType type, @Nullable SessionId sessionId, long timestamp, boolean local, - boolean read, boolean visible, long autoDeleteTimer); + boolean read, boolean visible, long autoDeleteTimer, + boolean isAutoDecline); void addSessionId(BdfDictionary meta, SessionId sessionId); diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java index 6b56089f8..d4ff9cb4e 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java @@ -24,6 +24,7 @@ import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_ import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AUTO_DELETE_TIMER; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_IS_AUTO_DECLINE; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_LOCAL; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_SESSION_ID; @@ -53,15 +54,24 @@ class MessageEncoderImpl implements MessageEncoder { public BdfDictionary encodeRequestMetadata(long timestamp, long autoDeleteTimer) { BdfDictionary meta = encodeMetadata(REQUEST, null, timestamp, - false, false, false, autoDeleteTimer); + autoDeleteTimer); meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, false); return meta; } + @Override + public BdfDictionary encodeMetadata(MessageType type, + @Nullable SessionId sessionId, long timestamp, + long autoDeleteTimer) { + return encodeMetadata(type, sessionId, timestamp, false, false, false, + autoDeleteTimer, false); + } + @Override public BdfDictionary encodeMetadata(MessageType type, @Nullable SessionId sessionId, long timestamp, boolean local, - boolean read, boolean visible, long autoDeleteTimer) { + boolean read, boolean visible, long autoDeleteTimer, + boolean isAutoDecline) { BdfDictionary meta = new BdfDictionary(); meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue()); if (sessionId != null) @@ -75,6 +85,9 @@ class MessageEncoderImpl implements MessageEncoder { if (autoDeleteTimer != NO_AUTO_DELETE_TIMER) { meta.put(MSG_KEY_AUTO_DELETE_TIMER, autoDeleteTimer); } + if (isAutoDecline) { + meta.put(MSG_KEY_IS_AUTO_DECLINE, isAutoDecline); + } return meta; } diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java index 2ae4e8daa..581f48c4c 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java @@ -14,11 +14,11 @@ class MessageMetadata { @Nullable private final SessionId sessionId; private final long timestamp, autoDeleteTimer; - private final boolean local, read, visible, available; + private final boolean local, read, visible, available, isAutoDecline; MessageMetadata(MessageType type, @Nullable SessionId sessionId, long timestamp, boolean local, boolean read, boolean visible, - boolean available, long autoDeleteTimer) { + boolean available, long autoDeleteTimer, boolean isAutoDecline) { this.type = type; this.sessionId = sessionId; this.timestamp = timestamp; @@ -27,6 +27,7 @@ class MessageMetadata { this.visible = visible; this.available = available; this.autoDeleteTimer = autoDeleteTimer; + this.isAutoDecline = isAutoDecline; } MessageType getMessageType() { @@ -61,4 +62,8 @@ class MessageMetadata { public long getAutoDeleteTimer() { return autoDeleteTimer; } + + public boolean isAutoDecline() { + return isAutoDecline; + } } diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java index 97d665ca1..9e0e33309 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java @@ -23,6 +23,7 @@ import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_ import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AUTO_DELETE_TIMER; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_IS_AUTO_DECLINE; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_LOCAL; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE; import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_SESSION_ID; @@ -46,7 +47,8 @@ class MessageParserImpl implements MessageParser { } @Override - public BdfDictionary getRequestsAvailableToAnswerQuery(SessionId sessionId) { + public BdfDictionary getRequestsAvailableToAnswerQuery( + SessionId sessionId) { return BdfDictionary.of( new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true), new BdfEntry(MSG_KEY_MESSAGE_TYPE, REQUEST.getValue()), @@ -68,8 +70,9 @@ class MessageParserImpl implements MessageParser { boolean visible = d.getBoolean(MSG_KEY_VISIBLE_IN_UI); boolean available = d.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false); long timer = d.getLong(MSG_KEY_AUTO_DELETE_TIMER, NO_AUTO_DELETE_TIMER); + boolean isAutoDecline = d.getBoolean(MSG_KEY_IS_AUTO_DECLINE, false); return new MessageMetadata(type, sessionId, timestamp, local, read, - visible, available, timer); + visible, available, timer, isAutoDecline); } @Override diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java index b9eb7aba3..8992b57bb 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java @@ -15,7 +15,14 @@ interface ProtocolEngine> { S onAcceptAction(Transaction txn, S session) throws DbException; - S onDeclineAction(Transaction txn, S session) throws DbException; + /** + * Declines an introduction. + * + * @param isAutoDecline true if automatically declined due to deletion + * and false if initiated by the user. + */ + S onDeclineAction(Transaction txn, S session, boolean isAutoDecline) + throws DbException; S onRequestMessage(Transaction txn, S session, RequestMessage m) throws DbException, FormatException; diff --git a/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java b/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java index 6f1081791..be35939f3 100644 --- a/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java @@ -102,7 +102,7 @@ public abstract class AbstractAutoDeleteTest extends @FunctionalInterface protected interface HeaderConsumer { - void accept(ConversationMessageHeader header); + void accept(ConversationMessageHeader header) throws DbException; } protected void forEachHeader(BriarIntegrationTestComponent component, diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/AutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/AutoDeleteIntegrationTest.java new file mode 100644 index 000000000..5ff0220d0 --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/AutoDeleteIntegrationTest.java @@ -0,0 +1,968 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.properties.TransportPropertyManager; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.system.TimeTravelModule; +import org.briarproject.bramble.test.TestDatabaseConfigModule; +import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; +import org.briarproject.briar.api.conversation.ConversationMessageHeader; +import org.briarproject.briar.api.introduction.IntroductionRequest; +import org.briarproject.briar.api.introduction.IntroductionResponse; +import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent; +import org.briarproject.briar.autodelete.AbstractAutoDeleteTest; +import org.briarproject.briar.test.BriarIntegrationTestComponent; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS; +import static org.briarproject.bramble.test.TestPluginConfigModule.SIMPLEX_TRANSPORT_ID; +import static org.briarproject.bramble.test.TestUtils.getTransportProperties; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; +import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; +import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSES; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_A; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_B; +import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED; +import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED; +import static org.briarproject.briar.introduction.IntroducerState.START; +import static org.briarproject.briar.test.TestEventListener.assertEvent; +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 AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { + + @Override + protected void createComponents() { + IntroductionIntegrationTestComponent component = + DaggerIntroductionIntegrationTestComponent.builder().build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(component); + component.inject(this); + + IntroductionIntegrationTestComponent c0 = + DaggerIntroductionIntegrationTestComponent.builder() + .testDatabaseConfigModule( + new TestDatabaseConfigModule(t0Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c0); + + IntroductionIntegrationTestComponent c1 = + DaggerIntroductionIntegrationTestComponent.builder() + .testDatabaseConfigModule( + new TestDatabaseConfigModule(t1Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c1); + + IntroductionIntegrationTestComponent c2 = + DaggerIntroductionIntegrationTestComponent.builder() + .testDatabaseConfigModule( + new TestDatabaseConfigModule(t2Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c2); + + this.c0 = c0; + this.c1 = c1; + this.c2 = c2; + + // Use different times to avoid creating identical messages that are + // treated as redundant copies of the same message (#1907) + try { + c0.getTimeTravel().setCurrentTimeMillis(startTime); + c1.getTimeTravel().setCurrentTimeMillis(startTime + 1); + c2.getTimeTravel().setCurrentTimeMillis(startTime + 2); + } catch (InterruptedException e) { + fail(); + } + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + addTransportProperties(); + } + + @Override + protected ConversationClient getConversationClient( + BriarIntegrationTestComponent component) { + return component.getIntroductionManager(); + } + + /* + * Basic tests. + * ASSERT timers are set on introduction messages + * ASSERT introduction messages self-destruct on all three sides + */ + + @Test + public void testIntroductionMessagesHaveTimer() throws Exception { + makeIntroduction(true, true); + assertIntroductionsArrived(); + + assertMessagesAmong0And1HaveTimerSet(1, 1); + assertMessagesAmong0And2HaveTimerSet(1, 1); + } + + @Test + public void testIntroductionAutoDeleteIntroducer() throws Exception { + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + + makeIntroduction(true, true); + assertIntroductionsArrived(); + + assertMessagesAmong0And1HaveTimerSet(1, 1); + assertMessagesAmong0And2HaveTimerSet(1, 1); + + // Sync the ack to 0 from 1 - this starts 0's timer + ack1To0(1); + waitForEvents(c0); + + // Before 0's timer elapses, the introducer should still see the + // introduction sent to 1 + timeTravel(c0, timerLatency - 1); + assertGroupCountAt0With1(1, 0); + assertGroupCountAt0With2(1, 0); + assertGroupCountAt1With0(1, 1); + assertGroupCountAt2With0(1, 1); + + // After 0's timer elapses, the introducer should have deleted the + // introduction sent to 1 + timeTravel(c0, 1); + assertGroupCountAt0With1(0, 0); + assertGroupCountAt0With2(1, 0); + assertGroupCountAt1With0(1, 1); + assertGroupCountAt2With0(1, 1); + + // Sync the ack to 0 from 2 - this starts 0's timer + ack2To0(1); + waitForEvents(c0); + + // Before 0's timer elapses, the introducer should still see the + // introduction sent to 2 + timeTravel(c0, timerLatency - 1); + assertGroupCountAt0With1(0, 0); + assertGroupCountAt0With2(1, 0); + assertGroupCountAt1With0(1, 1); + assertGroupCountAt2With0(1, 1); + + // After 0's timer elapses, the introducer should have deleted the + // introduction sent to 2 + timeTravel(c0, 1); + assertGroupCountAt0With1(0, 0); + assertGroupCountAt0With2(0, 0); + assertGroupCountAt1With0(1, 1); + assertGroupCountAt2With0(1, 1); + } + + @Test + public void testIntroductionAutoDeleteIntroducee() throws Exception { + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + + makeIntroduction(true, false); + + markMessagesRead(c1, contact0From1); + assertGroupCountAt1With0(1, 0); + assertGroupCountAt2With0(1, 1); + + // Travel in time at c1 unit 1ms before the deadline expires + timeTravel(c1, timerLatency - 1); + assertGroupCountAt0With1(1, 0); + assertGroupCountAt0With2(1, 0); + assertGroupCountAt2With0(1, 1); + // There is currently 1 message at 1 with 0, the introduction request + assertGroupCountAt1With0(1, 0); + forEachHeader(c1, contactId0From1, 1, h -> + assertTrue(h instanceof IntroductionRequest) + ); + + // After travelling in time one more 1ms, the introduction should be + // auto-declined. We should get an event signalling that the response + // has been sent. + IntroductionResponseReceivedEvent e = assertEvent(c1, + IntroductionResponseReceivedEvent.class, () -> + timeTravel(c1, 1) + ); + // the event should have correct values + assertEquals(contactId0From1, e.getContactId()); + IntroductionResponse response = e.getMessageHeader(); + assertEquals(author2.getName(), + response.getIntroducedAuthor().getName()); + assertTrue(response.isAutoDecline()); + + // these should not have changed + assertGroupCountAt0With1(1, 0); + assertGroupCountAt0With2(1, 0); + assertGroupCountAt2With0(1, 1); + // there is still 1 message at 1 with 0, but it should now be the new + // auto-decline message instead of the introduction + assertGroupCountAt1With0(1, 0); + forEachHeader(c1, contactId0From1, 1, h -> { + assertTrue(h instanceof IntroductionResponse); + IntroductionResponse r = (IntroductionResponse) h; + assertEquals(author2.getName(), r.getIntroducedAuthor().getName()); + assertTrue(r.isAutoDecline()); + }); + + // sync auto-decline to 0 + sync1To0(1, true); + waitForEvents(c0); + // auto-decline arrived at 0 + assertGroupCountAt0With1(2, 1); + + // forward auto-decline to 2 + sync0To2(1, true); + waitForEvents(c2); + // auto-decline arrived at 2 + assertGroupCountAt2With0(2, 2); + } + + /** + * Let one introducee accept, the other decline manually + * ASSERT accept and decline messages arrive and have timer on all sides + * ASSERT accept and decline get forwarded to the other introducee and that + * they all have timers set on all sides + * ASSERT that all messages self-destruct + */ + @Test + public void testIntroductionSessionManualDecline() throws Exception { + makeIntroduction(true, true); + + assertIntroducerStatus(AWAIT_RESPONSES); + + // mark messages as read on 1 and 2, this starts 1's and 2's timer for 0 + markMessagesRead(c1, contact0From1); + markMessagesRead(c2, contact0From2); + assertGroupCountAt1With0(1, 0); + assertGroupCountAt2With0(1, 0); + + respondToMostRecentIntroduction(c1, contactId0From1, true); + respondToMostRecentIntroduction(c2, contactId0From2, false); + sync1To0(1, true); + sync2To0(1, true); + waitForEvents(c0); + + // added the own responses + assertGroupCountAt1With0(2, 0); + assertGroupCountAt2With0(2, 0); + + // 0 has the sent introduction and the unread responses + assertGroupCountAt0With1(2, 1); + assertGroupCountAt0With2(2, 1); + + assertIntroducerStatus(START); + + markMessagesRead(c0, contact1From0); + markMessagesRead(c0, contact2From0); + assertGroupCountAt0With1(2, 0); + assertGroupCountAt0With2(2, 0); + + // forward responses from 0 to introducees + sync0To1(1, true); + sync0To2(1, true); + waitForEvents(c1); + waitForEvents(c2); + + // first contact receives the forwarded decline + assertGroupCountAt1With0(3, 1); + // second contact does not display an extra message because 2 declined + // themselves + assertGroupCountAt2With0(2, 0); + + markMessagesRead(c1, contact0From1); + assertGroupCountAt1With0(3, 0); + + assertMessagesAmong0And1HaveTimerSet(2, 3); + assertMessagesAmong0And2HaveTimerSet(2, 2); + + // Travel in time on all devices + timeTravel(c0); + timeTravel(c1); + timeTravel(c2); + + assertGroupCountAt0With1(0, 0); + assertGroupCountAt0With2(0, 0); + assertGroupCountAt1With0(0, 0); + assertGroupCountAt2With0(0, 0); + } + + /** + * Let introductions self-destruct at the introducer and one of the + * introducees + * ASSERT that auto-declines get sent and arrive + * ASSERT that auto-declines have the timer set + * ASSERT that declines get forwarded to other introducee + * ASSERT that forwarded declines have the timer set + * ASSERT that forwarded declines self-destruct + * ASSERT that a that a new introduction can succeed afterwards + */ + @Test + public void testIntroductionSessionAutoDecline() throws Exception { + makeIntroduction(true, false); + + assertIntroducerStatus(AWAIT_RESPONSES); + + // ack from 1 and 2 to 0. This starts 0's timer for 1 + ack1To0(1); + ack2To0(1); + waitForEvents(c0); + + // mark messages as read on 1 and 2, this starts 1's timer for 0 + markMessagesRead(c1, contact0From1); + markMessagesRead(c2, contact0From2); + assertGroupCountAt1With0(1, 0); + assertGroupCountAt2With0(1, 0); + + // Travel in time on all devices + timeTravel(c0); + timeTravel(c1); + timeTravel(c2); + + // assert that introductions have been deleted between 0 and 1 but not + // between 0 and 2 + assertGroupCountAt0With1(0, 0); + assertGroupCountAt0With2(1, 0); + // there is now 1 message, the auto-decline message + assertGroupCountAt1With0(1, 0); + assertGroupCountAt2With0(1, 0); + + // sync the auto-decline message from 1 to 0 + sync1To0(1, true); + waitForEvents(c0); + + // the auto-decline from 1 has arrived at 0 + assertGroupCountAt0With1(1, 1); + + // the session status has moved to A_DECLINED or B_DECLINED + assertIntroducerStatusFirstDeclined(); + + // sync the auto-decline message from 0 to 2 + sync0To2(1, true); + waitForEvents(c2); + // the auto-decline from 1 has arrived at 2 + assertGroupCountAt2With0(2, 1); + + // 0 and 1 still have the auto-decline message from 0 + assertGroupCountAt0With1(1, 1); + assertGroupCountAt1With0(1, 0); + + // make sure the auto-decline has the timer set at 0 and 1 + assertMessagesAmong0And1HaveTimerSet(1, 1); + + // ack message from 0 to 1 and make sure it self-destructs on 1 + ack0To1(1); + timeTravel(c1); + assertGroupCountAt1With0(0, 0); + + // mark message read on 0 and make sure it self-destructs on 0 + markMessagesRead(c0, contact1From0); + timeTravel(c0); + assertGroupCountAt0With1(0, 0); + + // assert that a that a new introduction can succeed afterwards: + // first decline from c2, assert we're in START state and then + // make the new introdution + respondToMostRecentIntroduction(c2, contactId0From2, false); + sync2To0(1, true); + waitForEvents(c0); + sync0To1(1, true); + waitForEvents(c1); + + assertIntroducerStatus(START); + assertNewIntroductionSucceeds(); + } + + @Test + public void testIntroductionAcceptHasTimer() throws Exception { + testIntroductionResponseHasTimer(true); + } + + @Test + public void testIntroductionDeclineHasTimer() throws Exception { + testIntroductionResponseHasTimer(false); + } + + private void testIntroductionResponseHasTimer(boolean accept) + throws Exception { + makeIntroduction(true, false); + assertIntroductionsArrived(); + + // check that all messages have the timer set + assertMessagesAmong0And1HaveTimerSet(1, 1); + assertMessagesAmong0And2HaveTimerNotSet(1, 1); + + respondToMostRecentIntroduction(c1, contactId0From1, accept); + sync1To0(1, true); + waitForEvents(c0); + + // check that response has arrived + assertGroupCountAt0With1(2, 1); + assertGroupCountAt1With0(2, 1); + assertMessagesAmong0And1HaveTimerSet(2, 2); + } + + @Test + public void testIntroductionAcceptSelfDestructs() throws Exception { + testIntroductionResponseSelfDestructs(true); + } + + @Test + public void testIntroductionDeclineSelfDestructs() throws Exception { + testIntroductionResponseSelfDestructs(false); + } + + private void testIntroductionResponseSelfDestructs(boolean accept) + throws Exception { + makeIntroduction(true, false); + assertIntroductionsArrived(); + + assertMessagesAmong0And1HaveTimerSet(1, 1); + assertMessagesAmong0And2HaveTimerNotSet(1, 1); + + respondToMostRecentIntroduction(c1, contactId0From1, accept); + sync1To0(1, true); + waitForEvents(c0); + + // Sync the ack to 1 - this starts 1's timer + ack0To1(1); + waitForEvents(c1); + + // check that response has arrived + assertGroupCountAt0With1(2, 1); + assertGroupCountAt1With0(2, 1); + assertMessagesAmong0And1HaveTimerSet(2, 2); + + // mark messages as read on 1, this starts 1's timer for 0 + markMessagesRead(c1, contact0From1); + assertGroupCountAt1With0(2, 0); + + // mark messages as read on 0, this starts 0's timer for 1 + markMessagesRead(c0, contact1From0); + assertGroupCountAt0With1(2, 0); + + // Travel in time on all devices + timeTravel(c0); + timeTravel(c1); + timeTravel(c2); + + // assert that introductions and responses have been deleted between + // 0 and 1 + assertGroupCountAt0With1(0, 0); + assertGroupCountAt1With0(0, 0); + } + + /* + * Tests that checks whether an introduction can still succeed properly + * after the introduction self-destructed at the introducer. + */ + + /** + * Let introductions self-destruct at the introducer only + * Let both introducees accept the introduction + * ASSERT that accept messages still get forwarded to the other introducer + * ASSERT that the introduction succeeds + * ASSERT all messages involved self-destruct eventually + */ + @Test + public void testSucceedAfterIntroducerSelfDestructed() throws Exception { + testSucceedAfterIntroducerSelfDestructed(false); + } + + /** + * Variant of the above test that also auto-deletes the responses at each + * introducee received from the respective other introducee. + */ + @Test + public void testSucceedAfterIntroducerAndResponsesSelfDestructed() + throws Exception { + testSucceedAfterIntroducerSelfDestructed(true); + } + + private void testSucceedAfterIntroducerSelfDestructed( + boolean autoDeleteResponsesBeforeSyncingAuthAndActivate) + throws Exception { + makeIntroduction(true, true); + + // ack from 1 and 2 to 0. This starts 0's timer for 1 and 2 + ack1To0(1); + ack2To0(1); + waitForEvents(c0); + + // Travel in time at 0 + timeTravel(c0); + + assertGroupCountAt0With1(0, 0); + assertGroupCountAt0With2(0, 0); + + // -> introductions self-destructed at the introducer only + + // introducees have got the introduction + assertGroupCountAt1With0(1, 1); + assertGroupCountAt2With0(1, 1); + + respondToMostRecentIntroduction(c1, contactId0From1, true); + respondToMostRecentIntroduction(c2, contactId0From2, true); + sync1To0(1, true); + sync2To0(1, true); + waitForEvents(c0); + + // introducees have got the own response + assertGroupCountAt1With0(2, 1); + assertGroupCountAt2With0(2, 1); + + // sync forwarded ACCEPT messages to introducees + sync0To1(1, true); + sync0To2(1, true); + waitForEvents(c1); + waitForEvents(c2); + + if (autoDeleteResponsesBeforeSyncingAuthAndActivate) { + assertGroupCountAt1With0(2, 1); + assertGroupCountAt2With0(2, 1); + + markMessagesRead(c1, contact0From1); + markMessagesRead(c2, contact0From2); + + assertGroupCountAt1With0(2, 0); + assertGroupCountAt2With0(2, 0); + + // Travel in time at 1 and 2 + timeTravel(c1); + timeTravel(c2); + + assertGroupCountAt1With0(0, 0); + assertGroupCountAt2With0(0, 0); + } + + syncAuthAndActivateMessages(); + + assertIntroductionSucceeded(); + } + + /* + * Group of three tests that check whether an introduction can still fail + * properly after the introduction self-destructed at the introducer. + *

+ * Let introductions self-destruct at the introducer only + * Variant 1: Let both introducees decline the introduction + * Variant 2: Let first introducee accept, second decline the introduction + * Variant 3: Let first introducee decline, second accept the introduction + * ASSERT that accept/decline messages still get forwarded to the other introducer + * ASSERT that the introduction does not succeed + * ASSERT that abort messages do get sent + * ASSERT all messages involved self-destruct eventually + * ASSERT that a new introduction can succeed afterwards + */ + + @Test + public void testFailAfterIntroducerSelfDestructedBothDecline() + throws Exception { + testFailAfterIntroducerSelfDestructed(false, false); + } + + @Test + public void testFailAfterIntroducerSelfDestructedFirstAccept() + throws Exception { + testFailAfterIntroducerSelfDestructed(true, false); + } + + @Test + public void testFailAfterIntroducerSelfDestructedSecondAccept() + throws Exception { + testFailAfterIntroducerSelfDestructed(false, true); + } + + private void testFailAfterIntroducerSelfDestructed(boolean firstAccepts, + boolean secondAccepts) throws Exception { + assertFalse(firstAccepts && secondAccepts); + + makeIntroduction(true, true); + + // ack from 1 and 2 to 0. This starts 0's timer for 1 and 2 + ack1To0(1); + ack2To0(1); + waitForEvents(c0); + + // Travel in time at 0 + timeTravel(c0); + + assertGroupCountAt0With1(0, 0); + assertGroupCountAt0With2(0, 0); + + // -> introductions self-destructed at the introducer only + + assertIntroducerStatus(AWAIT_RESPONSES); + assertIntroduceeStatus(c1, IntroduceeState.AWAIT_RESPONSES); + assertIntroduceeStatus(c2, IntroduceeState.AWAIT_RESPONSES); + + // introducees have got the introduction + assertGroupCountAt1With0(1, 1); + assertGroupCountAt2With0(1, 1); + + // first contact reads the introduction and responds + markMessagesRead(c1, contact0From1); + respondToMostRecentIntroduction(c1, contactId0From1, firstAccepts); + sync1To0(1, true); + waitForEvents(c0); + + if (firstAccepts) { + assertIntroducerStatusFirstAccepted(); + } else { + assertIntroducerStatusFirstDeclined(); + } + + // second contact reads the introduction and responds + markMessagesRead(c2, contact0From2); + respondToMostRecentIntroduction(c2, contactId0From2, secondAccepts); + sync2To0(1, true); + waitForEvents(c0); + + assertIntroducerStatus(START); + + // introducees have got the own response + assertGroupCountAt1With0(2, 0); + assertGroupCountAt2With0(2, 0); + + // sync forwarded ACCEPT/DECLINE messages to introducees + sync0To1(1, true); + waitForEvents(c1); + sync0To2(1, true); + waitForEvents(c2); + + assertIntroductionFailed(); + + if (firstAccepts) { + // one additional message, the other introducee's response + assertGroupCountAt1With0(3, 1); + } else { + assertGroupCountAt1With0(2, 0); + } + if (secondAccepts) { + // one additional message, the other introducee's response + assertGroupCountAt2With0(3, 1); + } else { + assertGroupCountAt2With0(2, 0); + } + + timeTravel(c1); + timeTravel(c2); + + if (firstAccepts) { + assertGroupCountAt1With0(1, 1); + } else { + assertGroupCountAt1With0(0, 0); + } + if (secondAccepts) { + assertGroupCountAt2With0(1, 1); + } else { + assertGroupCountAt2With0(0, 0); + } + + // -> if one of the introducees accepted, they still have got an unread + // decline from the other introducee + + if (firstAccepts) { + markMessagesRead(c1, contact0From1); + timeTravel(c1); + assertGroupCountAt1With0(0, 0); + } + if (secondAccepts) { + markMessagesRead(c2, contact0From2); + timeTravel(c2); + assertGroupCountAt2With0(0, 0); + } + + // make sure the introducees session status returned to START + assertIntroduceeStatus(c1, IntroduceeState.START); + assertIntroduceeStatus(c2, IntroduceeState.START); + + assertNewIntroductionSucceeds(); + } + + private void makeIntroduction(boolean enableTimer1, boolean enableTimer2) + throws Exception { + if (enableTimer1) { + setAutoDeleteTimer(c0, contact1From0.getId(), + MIN_AUTO_DELETE_TIMER_MS); + } + if (enableTimer2) { + setAutoDeleteTimer(c0, contact2From0.getId(), + MIN_AUTO_DELETE_TIMER_MS); + } + + // make introduction + c0.getIntroductionManager() + .makeIntroduction(contact1From0, contact2From0, "Hi!"); + + sync0To1(1, true); + sync0To2(1, true); + waitForEvents(c1); + waitForEvents(c2); + } + + private void respondToMostRecentIntroduction( + BriarIntegrationTestComponent c, ContactId contactId, + boolean accept) throws Exception { + List headers = + getMessageHeaders(c, contactId); + Collections.reverse(headers); + for (ConversationMessageHeader h : headers) { + if (h instanceof IntroductionRequest) { + IntroductionRequest ir = (IntroductionRequest) h; + c.getIntroductionManager().respondToIntroduction(contactId, + ir.getSessionId(), accept); + return; + } + } + fail("no introduction found"); + } + + private void markMessagesRead(BriarIntegrationTestComponent c, + Contact contact) throws Exception { + for (ConversationMessageHeader h : getMessageHeaders(c, + contact.getId())) { + markMessageRead(c, contact, h.getId()); + } + } + + private void syncAuthAndActivateMessages() throws Exception { + // sync first AUTH and its forward + sync1To0(1, true); + sync0To2(1, true); + + // sync second AUTH and its forward as well as the following ACTIVATE + sync2To0(2, true); + sync0To1(2, true); + + // sync second ACTIVATE and its forward + sync1To0(1, true); + sync0To2(1, true); + } + + private void timeTravel(BriarIntegrationTestComponent c) throws Exception { + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + timeTravel(c, timerLatency); + } + + private void timeTravel(BriarIntegrationTestComponent c, long timerLatency) + throws Exception { + c.getTimeTravel().addCurrentTimeMillis(timerLatency); + waitForEvents(c); + } + + private void assertIntroductionsArrived() throws DbException { + // check that introductions have arrived at the introducees + assertGroupCount(c0, contactId1From0, 1, 0); + assertGroupCount(c0, contactId2From0, 1, 0); + assertGroupCount(c1, contactId0From1, 1, 1); + assertGroupCount(c2, contactId0From2, 1, 1); + } + + private void assertGroupCountAt0With1(int messageCount, int unreadCount) + throws Exception { + assertGroupCount(c0, contactId1From0, messageCount, unreadCount); + assertEquals(messageCount, + getMessageHeaders(c0, contactId1From0).size()); + } + + private void assertGroupCountAt0With2(int messageCount, int unreadCount) + throws Exception { + assertGroupCount(c0, contactId2From0, messageCount, unreadCount); + assertEquals(messageCount, + getMessageHeaders(c0, contactId2From0).size()); + } + + private void assertGroupCountAt1With0(int messageCount, int unreadCount) + throws Exception { + assertGroupCount(c1, contactId0From1, messageCount, unreadCount); + assertEquals(messageCount, + getMessageHeaders(c1, contactId0From1).size()); + } + + private void assertGroupCountAt2With0(int messageCount, int unreadCount) + throws Exception { + assertGroupCount(c2, contactId0From2, messageCount, unreadCount); + assertEquals(messageCount, + getMessageHeaders(c2, contactId0From2).size()); + } + + private void assertMessagesAmong0And1HaveTimerSet(int numC0, int numC1) + throws Exception { + forEachHeader(c0, contactId1From0, numC0, h -> + assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer())); + forEachHeader(c1, contactId0From1, numC1, h -> + assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer())); + } + + private void assertMessagesAmong0And2HaveTimerSet(int numC0, int numC2) + throws Exception { + forEachHeader(c0, contactId2From0, numC0, h -> + assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer())); + forEachHeader(c2, contactId0From2, numC2, h -> + assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer())); + } + + private void assertMessagesAmong0And2HaveTimerNotSet(int numC0, int numC2) + throws Exception { + forEachHeader(c0, contactId2From0, numC0, h -> + assertEquals(NO_AUTO_DELETE_TIMER, h.getAutoDeleteTimer())); + forEachHeader(c2, contactId0From2, numC2, h -> + assertEquals(NO_AUTO_DELETE_TIMER, h.getAutoDeleteTimer())); + } + + private void assertIntroducerStatus(IntroducerState state) + throws DbException, FormatException { + IntroducerSession introducerSession = getIntroducerSession(); + assertEquals(state, introducerSession.getState()); + } + + private void assertIntroducerStatusFirstDeclined() + throws DbException, FormatException { + IntroductionCrypto introductionCrypto = + ((IntroductionIntegrationTestComponent) c0) + .getIntroductionCrypto(); + boolean alice = + introductionCrypto.isAlice(contact1From0.getAuthor().getId(), + contact2From0.getAuthor().getId()); + IntroducerSession introducerSession = getIntroducerSession(); + assertEquals(alice ? A_DECLINED : B_DECLINED, + introducerSession.getState()); + } + + private void assertIntroducerStatusFirstAccepted() + throws DbException, FormatException { + IntroductionCrypto introductionCrypto = + ((IntroductionIntegrationTestComponent) c0) + .getIntroductionCrypto(); + boolean alice = + introductionCrypto.isAlice(contact1From0.getAuthor().getId(), + contact2From0.getAuthor().getId()); + IntroducerSession introducerSession = getIntroducerSession(); + assertEquals(alice ? AWAIT_RESPONSE_B : AWAIT_RESPONSE_A, + introducerSession.getState()); + } + + private void assertIntroduceeStatus(BriarIntegrationTestComponent c, + IntroduceeState state) + throws DbException, FormatException { + IntroduceeSession introduceeSession = getIntroduceeSession(c); + assertEquals(state, introduceeSession.getState()); + } + + private void assertIntroductionSucceeded() throws DbException { + // make sure that introduced contacts have each other in their contact + // manager + assertTrue(contactManager1 + .contactExists(author2.getId(), author1.getId())); + assertTrue(contactManager2 + .contactExists(author1.getId(), author2.getId())); + + // make sure that introduced contacts are not verified + for (Contact c : contactManager1.getContacts()) { + if (c.getAuthor().equals(author2)) { + assertFalse(c.isVerified()); + } + } + for (Contact c : contactManager2.getContacts()) { + if (c.getAuthor().equals(author1)) { + assertFalse(c.isVerified()); + } + } + } + + private void assertIntroductionFailed() throws DbException { + // make sure that introduced contacts do not have each other in their + // contact manager + assertFalse(contactManager1 + .contactExists(author2.getId(), author1.getId())); + assertFalse(contactManager2 + .contactExists(author1.getId(), author2.getId())); + } + + private void assertNewIntroductionSucceeds() throws Exception { + makeIntroduction(false, false); + + respondToMostRecentIntroduction(c1, contactId0From1, true); + respondToMostRecentIntroduction(c2, contactId0From2, true); + sync1To0(1, true); + sync2To0(1, true); + waitForEvents(c0); + + // forward responses from 0 to introducees + sync0To1(1, true); + sync0To2(1, true); + waitForEvents(c1); + waitForEvents(c2); + + syncAuthAndActivateMessages(); + + assertIntroductionSucceeded(); + } + + private void addTransportProperties() throws Exception { + TransportPropertyManager tpm0 = c0.getTransportPropertyManager(); + TransportPropertyManager tpm1 = c1.getTransportPropertyManager(); + TransportPropertyManager tpm2 = c2.getTransportPropertyManager(); + + tpm0.mergeLocalProperties(SIMPLEX_TRANSPORT_ID, + getTransportProperties(2)); + sync0To1(1, true); + sync0To2(1, true); + + tpm1.mergeLocalProperties(SIMPLEX_TRANSPORT_ID, + getTransportProperties(2)); + sync1To0(1, true); + + tpm2.mergeLocalProperties(SIMPLEX_TRANSPORT_ID, + getTransportProperties(2)); + sync2To0(1, true); + } + + private IntroducerSession getIntroducerSession() + throws DbException, FormatException { + Map dicts = c0.getClientHelper() + .getMessageMetadataAsDictionary(getLocalGroup().getId()); + assertEquals(1, dicts.size()); + BdfDictionary d = dicts.values().iterator().next(); + SessionParser sessionParser = + ((IntroductionIntegrationTestComponent) c0).getSessionParser(); + return sessionParser.parseIntroducerSession(d); + } + + private IntroduceeSession getIntroduceeSession( + BriarIntegrationTestComponent c) + throws DbException, FormatException { + Map dicts = c.getClientHelper() + .getMessageMetadataAsDictionary(getLocalGroup().getId()); + assertEquals(1, dicts.size()); + BdfDictionary d = dicts.values().iterator().next(); + Group introducerGroup = + c2.getIntroductionManager().getContactGroup(contact0From2); + SessionParser sessionParser = + ((IntroductionIntegrationTestComponent) c).getSessionParser(); + return sessionParser + .parseIntroduceeSession(introducerGroup.getId(), d); + } + + private Group getLocalGroup() { + return contactGroupFactory + .createLocalGroup(CLIENT_ID, MAJOR_VERSION); + } + +} 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 20da1b770..952576c98 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 @@ -381,9 +381,6 @@ public class IntroductionIntegrationTest listener2.getResponse().getIntroducedAuthor().getName()); assertFalse(listener2.getResponse().canSucceed()); - // note how the introducer does not forward the second response, - // because after the first decline the protocol finished - assertFalse(listener1.succeeded); assertFalse(listener2.succeeded); diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java index 62e1a3d9f..eb39cf1d6 100644 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java @@ -49,6 +49,8 @@ interface IntroductionIntegrationTestComponent void inject(IntroductionCryptoIntegrationTest init); + void inject(AutoDeleteIntegrationTest init); + MessageEncoder getMessageEncoder(); MessageParser getMessageParser(); diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java index d73c1f5d6..7a3c9d12b 100644 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java @@ -640,8 +640,7 @@ public class IntroductionValidatorTest extends ValidatorTestCase { private void expectEncodeMetadata(MessageType type, long autoDeleteTimer) { context.checking(new Expectations() {{ oneOf(messageEncoder).encodeMetadata(type, sessionId, - message.getTimestamp(), false, false, false, - autoDeleteTimer); + message.getTimestamp(), autoDeleteTimer); will(returnValue(meta)); }}); } diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java index 15a872292..6f672242e 100644 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java @@ -107,7 +107,7 @@ public class MessageEncoderParserIntegrationTest extends BrambleTestCase { @Test public void testMessageMetadata() throws FormatException { BdfDictionary d = messageEncoder.encodeMetadata(ABORT, sessionId, - timestamp, false, true, false, MAX_AUTO_DELETE_TIMER_MS); + timestamp, false, true, false, MAX_AUTO_DELETE_TIMER_MS, false); MessageMetadata meta = messageParser.parseMetadata(d); assertEquals(ABORT, meta.getMessageType());