refactored omemo to take multiple recipients

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java  | 154 
src/main/java/eu/siacs/conversations/entities/Conversation.java          |  20 
src/main/java/eu/siacs/conversations/entities/MucOptions.java            |  11 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java          |   7 
src/main/java/eu/siacs/conversations/parser/MessageParser.java           |  14 
src/main/java/eu/siacs/conversations/parser/PresenceParser.java          |   8 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  40 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java        |  21 
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java           | 156 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java   |   2 
src/main/res/layout/activity_trust_keys.xml                              |  29 
src/main/res/layout/keys_card.xml                                        |  31 
12 files changed, 320 insertions(+), 173 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -81,13 +81,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		}
 	}
 
-	public boolean fetchMapHasErrors(Contact contact) {
-		Jid jid = contact.getJid().toBareJid();
-		if (deviceIds.get(jid) != null) {
-			for (Integer foreignId : this.deviceIds.get(jid)) {
-				AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
-				if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
-					return true;
+	public boolean fetchMapHasErrors(List<Jid> jids) {
+		for(Jid jid : jids) {
+			if (deviceIds.get(jid) != null) {
+				for (Integer foreignId : this.deviceIds.get(jid)) {
+					AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
+					if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
+						return true;
+					}
 				}
 			}
 		}
@@ -242,12 +243,29 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
 	}
 
-	public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) {
-		return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust);
+	public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) {
+		return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toString(), trust);
+	}
+
+	public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) {
+		Set<IdentityKey> keys = new HashSet<>();
+		for(Jid jid : jids) {
+			keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), trust));
+		}
+		return keys;
 	}
 
-	public long getNumTrustedKeys(Contact contact) {
-		return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
+	public long getNumTrustedKeys(Jid jid) {
+		return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString());
+	}
+
+	public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
+		for(Jid jid : jids) {
+			if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()) == 0) {
+				return true;
+			}
+		}
+		return false;
 	}
 
 	private AxolotlAddress getAddressForJid(Jid jid) {
@@ -259,11 +277,19 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		return new HashSet<>(this.sessions.getAll(ownAddress).values());
 	}
 
-	private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
+	private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) {
 		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
 		return new HashSet<>(this.sessions.getAll(contactAddress).values());
 	}
 
+	private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
+		HashSet<XmppAxolotlSession> sessions = new HashSet<>();
+		for(Jid jid : getCryptoTargets(conversation)) {
+			sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values());
+		}
+		return sessions;
+	}
+
 	public Set<String> getFingerprintsForOwnSessions() {
 		Set<String> fingerprints = new HashSet<>();
 		for (XmppAxolotlSession session : findOwnSessions()) {
@@ -274,15 +300,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	public Set<String> getFingerprintsForContact(final Contact contact) {
 		Set<String> fingerprints = new HashSet<>();
-		for (XmppAxolotlSession session : findSessionsforContact(contact)) {
+		for (XmppAxolotlSession session : findSessionsForContact(contact)) {
 			fingerprints.add(session.getFingerprint());
 		}
 		return fingerprints;
 	}
 
-	private boolean hasAny(Contact contact) {
-		AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
-		return sessions.hasAny(contactAddress);
+	private boolean hasAny(Jid jid) {
+		return sessions.hasAny(getAddressForJid(jid));
 	}
 
 	public boolean isPepBroken() {
@@ -594,10 +619,25 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		});
 	}
 
-	public boolean isContactAxolotlCapable(Contact contact) {
-		Jid jid = contact.getJid().toBareJid();
-		return hasAny(contact) ||
-				(deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
+	public boolean isConversationAxolotlCapable(Conversation conversation) {
+		final List<Jid> jids = getCryptoTargets(conversation);
+		for(Jid jid : jids) {
+			if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) {
+				return false;
+			}
+		}
+		return jids.size() > 0;
+	}
+
+	public List<Jid> getCryptoTargets(Conversation conversation) {
+		final List<Jid> jids;
+		if (conversation.getMode() == Conversation.MODE_SINGLE) {
+			jids = Arrays.asList(conversation.getJid().toBareJid());
+		} else {
+			jids = conversation.getMucOptions().getMembers();
+			jids.remove(account.getJid().toBareJid());
+		}
+		return jids;
 	}
 
 	public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
@@ -753,33 +793,31 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
-		return findDevicesWithoutSession(conversation.getContact().getJid().toBareJid());
-	}
-
-	public Set<AxolotlAddress> findDevicesWithoutSession(final Jid contactJid) {
-		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + contactJid);
 		Set<AxolotlAddress> addresses = new HashSet<>();
-		if (deviceIds.get(contactJid) != null) {
-			for (Integer foreignId : this.deviceIds.get(contactJid)) {
-				AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
-				if (sessions.get(address) == null) {
-					IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
-					if (identityKey != null) {
-						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
-						XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
-						sessions.put(address, session);
-					} else {
-						Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + contactJid + ":" + foreignId);
-						if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
-							addresses.add(address);
+		for(Jid jid : getCryptoTargets(conversation)) {
+			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
+			if (deviceIds.get(jid) != null) {
+				for (Integer foreignId : this.deviceIds.get(jid)) {
+					AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
+					if (sessions.get(address) == null) {
+						IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
+						if (identityKey != null) {
+							Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
+							XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
+							sessions.put(address, session);
 						} else {
-							Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken");
+							Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId);
+							if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
+								addresses.add(address);
+							} else {
+								Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken");
+							}
 						}
 					}
 				}
+			} else {
+				Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
 			}
-		} else {
-			Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
 		}
 		if (deviceIds.get(account.getJid().toBareJid()) != null) {
 			for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
@@ -827,7 +865,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public boolean trustedSessionVerified(final Conversation conversation) {
-		Set<XmppAxolotlSession> sessions = findSessionsforContact(conversation.getContact());
+		Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation);
 		sessions.addAll(findOwnSessions());
 		boolean verified = false;
 		for(XmppAxolotlSession session : sessions) {
@@ -842,26 +880,32 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		return verified;
 	}
 
-	public boolean hasPendingKeyFetches(Account account, Contact contact) {
+	public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
 		AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
-		AxolotlAddress foreignAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
-		return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
-				|| fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
-
+		if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) {
+			return true;
+		}
+		for(Jid jid : jids) {
+			AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toString(), 0);
+			if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
+				return true;
+			}
+		}
+		return false;
 	}
 
 	@Nullable
-	private XmppAxolotlMessage buildHeader(Contact contact) {
+	private XmppAxolotlMessage buildHeader(Conversation conversation) {
 		final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
-				contact.getJid().toBareJid(), getOwnDeviceId());
+				account.getJid().toBareJid(), getOwnDeviceId());
 
-		Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
+		Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
 		Set<XmppAxolotlSession> ownSessions = findOwnSessions();
-		if (contactSessions.isEmpty()) {
+		if (remoteSessions.isEmpty()) {
 			return null;
 		}
 		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
-		for (XmppAxolotlSession session : contactSessions) {
+		for (XmppAxolotlSession session : remoteSessions) {
 			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
 			axolotlMessage.addDevice(session);
 		}
@@ -876,7 +920,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	@Nullable
 	public XmppAxolotlMessage encrypt(Message message) {
-		XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
+		XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation());
 
 		if (axolotlMessage != null) {
 			final String content;
@@ -913,11 +957,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		});
 	}
 
-	public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
+	public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
 		executor.execute(new Runnable() {
 			@Override
 			public void run() {
-				XmppAxolotlMessage axolotlMessage = buildHeader(contact);
+				XmppAxolotlMessage axolotlMessage = buildHeader(conversation);
 				onMessageCreatedCallback.run(axolotlMessage);
 			}
 		});

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -659,8 +659,8 @@ public class Conversation extends AbstractEntity implements Blockable {
 		final AxolotlService axolotlService = getAccount().getAxolotlService();
 		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
 		if (next == -1) {
-			if (Config.X509_VERIFICATION && mode == MODE_SINGLE) {
-				if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
+			if (Config.X509_VERIFICATION) {
+				if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
 					return Message.ENCRYPTION_AXOLOTL;
 				} else {
 					return Message.ENCRYPTION_NONE;
@@ -673,16 +673,20 @@ public class Conversation extends AbstractEntity implements Blockable {
 				next = outgoing;
 			}
 		}
-		if (!Config.supportUnencrypted()
-				&& (mode == MODE_SINGLE || Config.supportOpenPgpOnly())
-				&& next <= 0) {
-			if (Config.supportOmemo() && (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact()) || !Config.multipleEncryptionChoices())) {
+
+		if (!Config.supportUnencrypted() && next <= 0) {
+			if (Config.supportOmemo()
+					&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
 				return Message.ENCRYPTION_AXOLOTL;
-			} else if (Config.supportOtr()) {
+			} else if (Config.supportOtr() && mode == MODE_SINGLE) {
 				return Message.ENCRYPTION_OTR;
-			} else if (Config.supportOpenPgp()) {
+			} else if (Config.supportOpenPgp()
+					&& (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
 				return Message.ENCRYPTION_PGP;
 			}
+		} else if (next == Message.ENCRYPTION_AXOLOTL
+				&& (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
+			next = Message.ENCRYPTION_NONE;
 		}
 		return next;
 	}

src/main/java/eu/siacs/conversations/entities/MucOptions.java 🔗

@@ -4,9 +4,11 @@ import android.annotation.SuppressLint;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.xmpp.forms.Data;
@@ -245,6 +247,7 @@ public class MucOptions {
 
 	private Account account;
 	private final Map<String, User> users = Collections.synchronizedMap(new LinkedHashMap<String, User>());
+	private final Set<Jid> members = Collections.synchronizedSet(new HashSet<Jid>());
 	private List<String> features = new ArrayList<>();
 	private Data form = new Data();
 	private Conversation conversation;
@@ -501,4 +504,12 @@ public class MucOptions {
 	public Conversation getConversation() {
 		return this.conversation;
 	}
+
+	public void putMember(Jid jid) {
+		members.add(jid);
+	}
+
+	public List<Jid> getMembers() {
+		return new ArrayList<>(members);
+	}
 }

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -335,4 +335,11 @@ public class IqGenerator extends AbstractGenerator {
 		enable.addChild(data);
 		return packet;
 	}
+
+	public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
+		IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
+		packet.setTo(conversation.getJid().toBareJid());
+		packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation",affiliation);
+		return packet;
+	}
 }

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -103,7 +103,7 @@ public class MessageParser extends AbstractParser implements
 		}
 	}
 
-	private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
+	private Message parseAxolotlChat(Element axolotlMessage, Jid from,  Conversation conversation, int status) {
 		Message finishedMessage = null;
 		AxolotlService service = conversation.getAccount().getAxolotlService();
 		XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
@@ -357,7 +357,17 @@ public class MessageParser extends AbstractParser implements
 			} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
 				message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
 			} else if (axolotlEncrypted != null && Config.supportOmemo()) {
-				message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
+				Jid origin;
+				if (conversation.getMode() == Conversation.MODE_MULTI) {
+					origin = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart());
+					if (origin == null) {
+						Log.d(Config.LOGTAG,"axolotl message in non anonymous conference received");
+						return;
+					}
+				} else {
+					origin = from;
+				}
+				message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status);
 				if (message == null) {
 					return;
 				}

src/main/java/eu/siacs/conversations/parser/PresenceParser.java 🔗

@@ -64,10 +64,14 @@ public class PresenceParser extends AbstractParser implements
 					Element item = x.findChild("item");
 					if (item != null && !from.isBareJid()) {
 						mucOptions.setError(MucOptions.Error.NONE);
-						MucOptions.User user = new MucOptions.User(mucOptions,from);
+						MucOptions.User user = new MucOptions.User(mucOptions, from);
 						user.setAffiliation(item.getAttribute("affiliation"));
 						user.setRole(item.getAttribute("role"));
-						user.setJid(item.getAttributeAsJid("jid"));
+						Jid real = item.getAttributeAsJid("jid");
+						if (real != null) {
+							user.setJid(real);
+							mucOptions.putMember(real.toBareJid());
+						}
 						if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) {
 							mucOptions.setOnline();
 							mucOptions.setSelf(user);

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -1784,6 +1784,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 					Account account = conversation.getAccount();
 					final String nick = conversation.getMucOptions().getProposedNick();
 					final Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
+					final MucOptions mucOptions = conversation.getMucOptions();
 					Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
 					PresencePacket packet = new PresencePacket();
 					packet.setFrom(conversation.getAccount().getJid());
@@ -1793,7 +1794,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 						x.addChild("password").setContent(conversation.getMucOptions().getPassword());
 					}
 
-					if (conversation.getMucOptions().mamSupport()) {
+					if (mucOptions.mamSupport()) {
 						// Use MAM instead of the limited muc history to get history
 						x.addChild("history").setAttribute("maxchars", "0");
 					} else {
@@ -1812,9 +1813,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 						conversation.setContactJid(joinJid);
 						databaseBackend.updateConversation(conversation);
 					}
-					if (conversation.getMucOptions().mamSupport()) {
+
+					if (mucOptions.mamSupport()) {
 						getMessageArchiveService().catchupMUC(conversation);
 					}
+					if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
+						fetchConferenceMembers(conversation);
+					}
 					sendUnsentMessages(conversation);
 				}
 
@@ -1838,6 +1843,37 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		}
 	}
 
+	private void fetchConferenceMembers(final Conversation conversation) {
+		final Account account = conversation.getAccount();
+		final String[] affiliations = {"member","admin","owner"};
+		OnIqPacketReceived callback = new OnIqPacketReceived() {
+
+			private int i = 0;
+
+			@Override
+			public void onIqPacketReceived(Account account, IqPacket packet) {
+				Element query = packet.query("http://jabber.org/protocol/muc#admin");
+				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
+					for(Element child : query.getChildren()) {
+						if ("item".equals(child.getName())) {
+							conversation.getMucOptions().putMember(child.getAttributeAsJid("jid"));
+						}
+					}
+				} else {
+					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
+				}
+				++i;
+				if (i >= affiliations.length) {
+					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
+				}
+			}
+		};
+		for(String affiliation : affiliations) {
+			sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
+		}
+		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching members for "+conversation.getName());
+	}
+
 	public void providePasswordForMuc(Conversation conversation, String password) {
 		if (conversation.getMode() == Conversation.MODE_MULTI) {
 			conversation.getMucOptions().setPassword(password);

src/main/java/eu/siacs/conversations/ui/ConversationActivity.java 🔗

@@ -408,7 +408,7 @@ public class ConversationActivity extends XmppActivity
 					menuContactDetails.setVisible(false);
 					menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating());
 					menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
-					menuSecure.setVisible(Config.supportOpenPgp() && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
+					menuSecure.setVisible((Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
 				} else {
 					menuMucDetails.setVisible(false);
 					menuSecure.setVisible(Config.multipleEncryptionChoices());
@@ -856,8 +856,8 @@ public class ConversationActivity extends XmppActivity
 			axolotl.setVisible(Config.supportOmemo());
 			if (conversation.getMode() == Conversation.MODE_MULTI) {
 				otr.setVisible(false);
-				axolotl.setVisible(false);
-			} else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
+			}
+			if (!conversation.getAccount().getAxolotlService().isConversationAxolotlCapable(conversation)) {
 				axolotl.setEnabled(false);
 			}
 			switch (conversation.getNextEncryption()) {
@@ -1530,18 +1530,21 @@ public class ConversationActivity extends XmppActivity
 
 	protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
 		AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
-		Contact contact = mSelectedConversation.getContact();
+		final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
 		boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
-		boolean hasUndecidedContact = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED,contact).isEmpty();
+		boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, targets).isEmpty();
 		boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
-		boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0;
-		if(hasUndecidedOwn || hasUndecidedContact || hasPendingKeys || hasNoTrustedKeys) {
+		boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
+		if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys) {
 			axolotlService.createSessionsIfNeeded(mSelectedConversation);
 			Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
-			intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString());
+			String[] contacts = new String[targets.size()];
+			for(int i = 0; i < contacts.length; ++i) {
+				contacts[i] = targets.get(i).toString();
+			}
+			intent.putExtra("contacts", contacts);
 			intent.putExtra(EXTRA_ACCOUNT, mSelectedConversation.getAccount().getJid().toBareJid().toString());
 			intent.putExtra("choice", attachmentChoice);
-			intent.putExtra("has_no_trusted", hasNoTrustedKeys);
 			startActivityForResult(intent, requestCode);
 			return true;
 		} else {

src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java 🔗

@@ -12,7 +12,9 @@ import android.widget.Toast;
 
 import org.whispersystems.libaxolotl.IdentityKey;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -20,32 +22,28 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
 public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
 	private Jid accountJid;
-	private Jid contactJid;
+	private List<Jid> contactJids;
 
-	private Contact contact;
 	private Account mAccount;
 	private TextView keyErrorMessage;
 	private LinearLayout keyErrorMessageCard;
 	private TextView ownKeysTitle;
 	private LinearLayout ownKeys;
 	private LinearLayout ownKeysCard;
-	private TextView foreignKeysTitle;
 	private LinearLayout foreignKeys;
-	private LinearLayout foreignKeysCard;
 	private Button mSaveButton;
 	private Button mCancelButton;
 
 	private AxolotlService.FetchStatus lastFetchReport = AxolotlService.FetchStatus.SUCCESS;
 
 	private final Map<String, Boolean> ownKeysToTrust = new HashMap<>();
-	private final Map<String, Boolean> foreignKeysToTrust = new HashMap<>();
+	private final Map<Jid,Map<String, Boolean>> foreignKeysToTrust = new HashMap<>();
 
 	private final OnClickListener mSaveButtonListener = new OnClickListener() {
 		@Override
@@ -69,15 +67,6 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 		populateView();
 	}
 
-	@Override
-	protected String getShareableUri() {
-		if (contact != null) {
-			return contact.getShareableUri();
-		} else {
-			return "";
-		}
-	}
-
 	@Override
 	protected void onCreate(final Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
@@ -86,9 +75,13 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 			this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT));
 		} catch (final InvalidJidException ignored) {
 		}
-		try {
-			this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact"));
-		} catch (final InvalidJidException ignored) {
+		this.contactJids = new ArrayList<>();
+		for(String jid : getIntent().getStringArrayExtra("contacts")) {
+			try {
+				this.contactJids.add(Jid.fromString(jid));
+			} catch (InvalidJidException e) {
+				e.printStackTrace();
+			}
 		}
 
 		keyErrorMessageCard = (LinearLayout) findViewById(R.id.key_error_message_card);
@@ -96,9 +89,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 		ownKeysTitle = (TextView) findViewById(R.id.own_keys_title);
 		ownKeys = (LinearLayout) findViewById(R.id.own_keys_details);
 		ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card);
-		foreignKeysTitle = (TextView) findViewById(R.id.foreign_keys_title);
-		foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys_details);
-		foreignKeysCard = (LinearLayout) findViewById(R.id.foreign_keys_card);
+		foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys);
 		mCancelButton = (Button) findViewById(R.id.cancel_button);
 		mCancelButton.setOnClickListener(mCancelButtonListener);
 		mSaveButton = (Button) findViewById(R.id.save_button);
@@ -119,7 +110,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 		boolean hasForeignKeys = false;
 		for(final String fingerprint : ownKeysToTrust.keySet()) {
 			hasOwnKeys = true;
-			addFingerprintRowWithListeners(ownKeys, contact.getAccount(), fingerprint, false,
+			addFingerprintRowWithListeners(ownKeys, mAccount, fingerprint, false,
 					XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)), false,
 					new CompoundButton.OnCheckedChangeListener() {
 						@Override
@@ -132,30 +123,36 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 					null
 			);
 		}
-		for(final String fingerprint : foreignKeysToTrust.keySet()) {
-			hasForeignKeys = true;
-			addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), fingerprint, false,
-					XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(fingerprint)), false,
-					new CompoundButton.OnCheckedChangeListener() {
-						@Override
-						public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-							foreignKeysToTrust.put(fingerprint, isChecked);
-							lockOrUnlockAsNeeded();
-						}
-					},
-					null,
-					null
-			);
-		}
 
-		if(hasOwnKeys) {
-			ownKeysTitle.setText(accountJid.toString());
-			ownKeysCard.setVisibility(View.VISIBLE);
-		}
-		if(hasForeignKeys) {
-			foreignKeysTitle.setText(contactJid.toString());
-			foreignKeysCard.setVisibility(View.VISIBLE);
+		synchronized (this.foreignKeysToTrust) {
+			for (Map.Entry<Jid, Map<String, Boolean>> entry : foreignKeysToTrust.entrySet()) {
+				final LinearLayout layout = (LinearLayout) getLayoutInflater().inflate(R.layout.keys_card, foreignKeys, false);
+				final TextView header = (TextView) layout.findViewById(R.id.foreign_keys_title);
+				final LinearLayout keysContainer = (LinearLayout) layout.findViewById(R.id.foreign_keys_details);
+				header.setText(entry.getKey().toString());
+				final Map<String, Boolean> fingerprints = entry.getValue();
+				for (final String fingerprint : fingerprints.keySet()) {
+					hasForeignKeys = true;
+					addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false,
+							XmppAxolotlSession.Trust.fromBoolean(fingerprints.get(fingerprint)), false,
+							new CompoundButton.OnCheckedChangeListener() {
+								@Override
+								public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+									fingerprints.put(fingerprint, isChecked);
+									lockOrUnlockAsNeeded();
+								}
+							},
+							null,
+							null
+					);
+				}
+				foreignKeys.addView(layout);
+			}
 		}
+
+		ownKeysTitle.setText(accountJid.toString());
+		ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE);
+		foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE);
 		if(hasPendingKeyFetches()) {
 			setFetching();
 			lock();
@@ -163,13 +160,15 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 			if (!hasForeignKeys && hasNoOtherTrustedKeys()) {
 				keyErrorMessageCard.setVisibility(View.VISIBLE);
 				if (lastFetchReport == AxolotlService.FetchStatus.ERROR
-						|| contact.getAccount().getAxolotlService().fetchMapHasErrors(contact)) {
+						|| mAccount.getAxolotlService().fetchMapHasErrors(contactJids)) {
 					keyErrorMessage.setText(R.string.error_no_keys_to_trust_server_error);
 				} else {
 					keyErrorMessage.setText(R.string.error_no_keys_to_trust);
 				}
-				ownKeys.removeAllViews(); ownKeysCard.setVisibility(View.GONE);
-				foreignKeys.removeAllViews(); foreignKeysCard.setVisibility(View.GONE);
+				ownKeys.removeAllViews();
+				ownKeysCard.setVisibility(View.GONE);
+				foreignKeys.removeAllViews();
+				foreignKeys.setVisibility(View.GONE);
 			}
 			lockOrUnlockAsNeeded();
 			setDone();
@@ -178,45 +177,56 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 
 	private boolean reloadFingerprints() {
 		ownKeysToTrust.clear();
-		foreignKeysToTrust.clear();
 		AxolotlService service = this.mAccount.getAxolotlService();
 		Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
-		Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact);
-		if (hasNoOtherTrustedKeys() && ownKeysSet.size() == 0) {
-			foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact));
-		}
 		for(final IdentityKey identityKey : ownKeysSet) {
 			if(!ownKeysToTrust.containsKey(identityKey)) {
 				ownKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
 			}
 		}
-		for(final IdentityKey identityKey : foreignKeysSet) {
-			if(!foreignKeysToTrust.containsKey(identityKey)) {
-				foreignKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
+		synchronized (this.foreignKeysToTrust) {
+			foreignKeysToTrust.clear();
+			for (Jid jid : contactJids) {
+				Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, jid);
+				if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) {
+					foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, jid));
+				}
+				Map<String, Boolean> foreignFingerprints = new HashMap<>();
+				for (final IdentityKey identityKey : foreignKeysSet) {
+					if (!foreignFingerprints.containsKey(identityKey)) {
+						foreignFingerprints.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
+					}
+				}
+				if (foreignFingerprints.size() > 0) {
+					foreignKeysToTrust.put(jid, foreignFingerprints);
+				}
 			}
 		}
-		return ownKeysSet.size() + foreignKeysSet.size() > 0;
+		return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
 	}
 
 	@Override
 	public void onBackendConnected() {
-		if ((accountJid != null) && (contactJid != null)) {
+		if (accountJid != null) {
 			this.mAccount = xmppConnectionService.findAccountByJid(accountJid);
 			if (this.mAccount == null) {
 				return;
 			}
-			this.contact = this.mAccount.getRoster().getContact(contactJid);
 			reloadFingerprints();
 			populateView();
 		}
 	}
 
 	private boolean hasNoOtherTrustedKeys() {
+		return mAccount == null || mAccount.getAxolotlService().anyTargetHasNoTrustedKeys(contactJids);
+	}
+
+	private boolean hasNoOtherTrustedKeys(Jid contact) {
 		return mAccount == null || mAccount.getAxolotlService().getNumTrustedKeys(contact) == 0;
 	}
 
 	private boolean hasPendingKeyFetches() {
-		return mAccount != null && contact != null && mAccount.getAxolotlService().hasPendingKeyFetches(mAccount,contact);
+		return mAccount != null && mAccount.getAxolotlService().hasPendingKeyFetches(mAccount, contactJids);
 	}
 
 
@@ -262,14 +272,18 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 
 	private void commitTrusts() {
 		for(final String fingerprint :ownKeysToTrust.keySet()) {
-			contact.getAccount().getAxolotlService().setFingerprintTrust(
+			mAccount.getAxolotlService().setFingerprintTrust(
 					fingerprint,
 					XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)));
 		}
-		for(final String fingerprint:foreignKeysToTrust.keySet()) {
-			contact.getAccount().getAxolotlService().setFingerprintTrust(
-					fingerprint,
-					XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(fingerprint)));
+		synchronized (this.foreignKeysToTrust) {
+			for (Map<String, Boolean> value : foreignKeysToTrust.values()) {
+				for (final String fingerprint : value.keySet()) {
+					mAccount.getAxolotlService().setFingerprintTrust(
+							fingerprint,
+							XmppAxolotlSession.Trust.fromBoolean(value.get(fingerprint)));
+				}
+			}
 		}
 	}
 
@@ -284,11 +298,17 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
 	}
 
 	private void lockOrUnlockAsNeeded() {
-		if (hasNoOtherTrustedKeys() && !foreignKeysToTrust.values().contains(true)){
-			lock();
-		} else {
-			unlock();
+		synchronized (this.foreignKeysToTrust) {
+			for (Jid jid : contactJids) {
+				Map<String, Boolean> fingerprints = foreignKeysToTrust.get(jid);
+				if (hasNoOtherTrustedKeys(jid) && (fingerprints == null || !fingerprints.values().contains(true))) {
+					lock();
+					return;
+				}
+			}
 		}
+		unlock();
+
 	}
 
 	private void setDone() {

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java 🔗

@@ -218,7 +218,7 @@ public class JingleConnection implements Transferable {
 	public void init(final Message message) {
 		if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 			Conversation conversation = message.getConversation();
-			conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation.getContact(), new OnMessageCreatedCallback() {
+			conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, new OnMessageCreatedCallback() {
 				@Override
 				public void run(XmppAxolotlMessage xmppAxolotlMessage) {
 					if (xmppAxolotlMessage != null) {

src/main/res/layout/activity_trust_keys.xml 🔗

@@ -80,34 +80,11 @@
             </LinearLayout>
 
             <LinearLayout
-                android:id="@+id/foreign_keys_card"
+                android:id="@+id/foreign_keys"
                 android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginLeft="@dimen/activity_horizontal_margin"
-                android:layout_marginRight="@dimen/activity_horizontal_margin"
-                android:layout_marginTop="@dimen/activity_vertical_margin"
-                android:layout_marginBottom="@dimen/activity_vertical_margin"
-                android:background="@drawable/infocard_border"
-                android:orientation="vertical"
-                android:padding="@dimen/infocard_padding"
-                android:visibility="gone">
-
-                <TextView
-                    android:id="@+id/foreign_keys_title"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:textColor="@color/black87"
-                    android:textSize="?attr/TextSizeHeadline"
-                    android:textStyle="bold"/>
-
-                <LinearLayout
-                    android:id="@+id/foreign_keys_details"
-                    android:layout_width="fill_parent"
-                    android:layout_height="wrap_content"
-                    android:divider="?android:dividerHorizontal"
-                    android:showDividers="middle"
-                    android:orientation="vertical">
-                </LinearLayout>
+                android:visibility="gone"
+                android:orientation="vertical">
 
             </LinearLayout>
 

src/main/res/layout/keys_card.xml 🔗

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+android:id="@+id/foreign_keys_card"
+android:layout_width="fill_parent"
+android:layout_height="wrap_content"
+android:layout_marginLeft="@dimen/activity_horizontal_margin"
+android:layout_marginRight="@dimen/activity_horizontal_margin"
+android:layout_marginTop="@dimen/activity_vertical_margin"
+android:layout_marginBottom="@dimen/activity_vertical_margin"
+android:background="@drawable/infocard_border"
+android:orientation="vertical"
+android:padding="@dimen/infocard_padding">
+
+<TextView
+    android:id="@+id/foreign_keys_title"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:textColor="@color/black87"
+    android:textSize="?attr/TextSizeHeadline"
+    android:textStyle="bold"/>
+
+<LinearLayout
+    android:id="@+id/foreign_keys_details"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:divider="?android:dividerHorizontal"
+    android:showDividers="middle"
+    android:orientation="vertical">
+</LinearLayout>
+
+</LinearLayout>