ground work for omemo dtls verification

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java                        | 119 
src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java                    |   4 
src/main/java/eu/siacs/conversations/xml/Namespace.java                                        |   1 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java                      |  84 
src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerification.java                        |  83 
src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java               |  19 
src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java                            |  32 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java              |   2 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/OmemoVerifiedIceUdpTransportInfo.java |  27 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java                          |  34 
10 files changed, 387 insertions(+), 18 deletions(-)

Detailed changes

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

@@ -8,6 +8,8 @@ import android.util.Pair;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.google.common.collect.ImmutableMap;
+
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.whispersystems.libsignal.IdentityKey;
 import org.whispersystems.libsignal.IdentityKeyPair;
@@ -49,9 +51,15 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
 import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.OmemoVerification;
+import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
+import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.pep.PublishOptions;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
@@ -1198,6 +1206,91 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         });
     }
 
+    public OmemoVerifiedIceUdpTransportInfo encrypt(final IceUdpTransportInfo element, final XmppAxolotlSession session) throws CryptoFailedException {
+        final OmemoVerifiedIceUdpTransportInfo transportInfo = new OmemoVerifiedIceUdpTransportInfo();
+        transportInfo.setAttributes(element.getAttributes());
+        for (final Element child : element.getChildren()) {
+            if ("fingerprint".equals(child.getName()) && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) {
+                final Element fingerprint = new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION);
+                fingerprint.setAttribute("setup", child.getAttribute("setup"));
+                fingerprint.setAttribute("hash", child.getAttribute("hash"));
+                final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
+                final String content = child.getContent();
+                axolotlMessage.encrypt(content);
+                axolotlMessage.addDevice(session);
+                fingerprint.addChild(axolotlMessage.toElement());
+                transportInfo.addChild(fingerprint);
+            } else {
+                transportInfo.addChild(child);
+            }
+        }
+        return transportInfo;
+    }
+
+
+    public OmemoVerifiedPayload<OmemoVerifiedRtpContentMap> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) throws CryptoFailedException {
+        final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
+        final XmppAxolotlSession session = sessions.get(address);
+        final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
+        final OmemoVerification omemoVerification = new OmemoVerification();
+        omemoVerification.setDeviceId(deviceId);
+        omemoVerification.setSessionFingerprint(session.getFingerprint());
+        for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : rtpContentMap.contents.entrySet()) {
+            final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
+            final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
+            descriptionTransportBuilder.put(
+                    content.getKey(),
+                    new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo)
+            );
+        }
+        return new OmemoVerifiedPayload<>(
+                omemoVerification,
+                new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build())
+        );
+    }
+
+    public OmemoVerifiedPayload<RtpContentMap> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) throws CryptoFailedException {
+        final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
+        final OmemoVerification omemoVerification = new OmemoVerification();
+        for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
+            final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
+            final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from);
+            omemoVerification.setOrEnsureEqual(decryptedTransport);
+            descriptionTransportBuilder.put(
+                    content.getKey(),
+                    new RtpContentMap.DescriptionTransport(descriptionTransport.description, decryptedTransport.payload)
+            );
+        }
+        return new OmemoVerifiedPayload<>(
+                omemoVerification,
+                new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build())
+        );
+    }
+
+    public OmemoVerifiedPayload<IceUdpTransportInfo> decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from) throws CryptoFailedException {
+        final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo();
+        transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes());
+        final OmemoVerification omemoVerification = new OmemoVerification();
+        for (final Element child : verifiedIceUdpTransportInfo.getChildren()) {
+            if ("fingerprint".equals(child.getName()) && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) {
+                final Element fingerprint = new Element("fingerprint", Namespace.JINGLE_APPS_DTLS);
+                fingerprint.setAttribute("setup", child.getAttribute("setup"));
+                fingerprint.setAttribute("hash", child.getAttribute("hash"));
+                final Element encrypted = child.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
+                final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, from.asBareJid());
+                final XmppAxolotlSession session = getReceivingSession(xmppAxolotlMessage);
+                final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = xmppAxolotlMessage.decrypt(session, getOwnDeviceId());
+                fingerprint.setContent(plaintext.getPlaintext());
+                omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
+                omemoVerification.setSessionFingerprint(plaintext.getFingerprint());
+                transportInfo.addChild(fingerprint);
+            } else {
+                transportInfo.addChild(child);
+            }
+        }
+        return new OmemoVerifiedPayload<>(omemoVerification, transportInfo);
+    }
+
     public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
         executor.execute(new Runnable() {
             @Override
@@ -1267,7 +1360,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         } catch (final BrokenSessionException e) {
             throw e;
         } catch (final OutdatedSenderException e) {
-            Log.e(Config.LOGTAG,account.getJid().asBareJid()+": "+e.getMessage());
+            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
             throw e;
         } catch (CryptoFailedException e) {
             Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e);
@@ -1565,4 +1658,28 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             }
         }
     }
+
+    public static class OmemoVerifiedPayload<T> {
+        private final int deviceId;
+        private final String fingerprint;
+        private final T payload;
+
+        private OmemoVerifiedPayload(OmemoVerification omemoVerification, T payload) {
+            this.deviceId = omemoVerification.getDeviceId();
+            this.fingerprint = omemoVerification.getFingerprint();
+            this.payload = payload;
+        }
+
+        public int getDeviceId() {
+            return deviceId;
+        }
+
+        public String getFingerprint() {
+            return fingerprint;
+        }
+
+        public T getPayload() {
+            return payload;
+        }
+    }
 }

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

@@ -59,7 +59,7 @@ public class XmppAxolotlMessage {
             switch (keyElement.getName()) {
                 case KEYTAG:
                     try {
-                        Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
+                        int recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
                         byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
                         boolean isPreKey = keyElement.getAttributeAsBoolean("prekey");
                         this.keys.add(new XmppAxolotlSession.AxolotlKey(recipientId, key, isPreKey));
@@ -145,7 +145,7 @@ public class XmppAxolotlMessage {
         return ciphertext != null;
     }
 
-    void encrypt(String plaintext) throws CryptoFailedException {
+    void encrypt(final String plaintext) throws CryptoFailedException {
         try {
             SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
             IvParameterSpec ivSpec = new IvParameterSpec(iv);

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -53,4 +53,5 @@ public final class Namespace {
     public static final String INVITE = "urn:xmpp:invite";
     public static final String PARS = "urn:xmpp:pars:0";
     public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
+    public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
 }

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

@@ -30,6 +30,8 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Conversational;
@@ -43,6 +45,7 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
@@ -123,6 +126,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
     private final ArrayDeque<Set<Map.Entry<String, RtpContentMap.DescriptionTransport>>> pendingIceCandidates = new ArrayDeque<>();
+    private final OmemoVerification omemoVerification = new OmemoVerification();
     private final Message message;
     private State state = State.NULL;
     private StateTransitionException stateTransitionException;
@@ -290,6 +294,25 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
     }
 
+    private RtpContentMap receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) {
+        final RtpContentMap receivedContentMap = RtpContentMap.of(jinglePacket);
+        if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) {
+            final AxolotlService.OmemoVerifiedPayload<RtpContentMap> omemoVerifiedPayload;
+            try {
+                omemoVerifiedPayload = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with);
+            } catch (final CryptoFailedException e) {
+                throw new SecurityException("Unable to verify DTLS Fingerprint with OMEMO", e);
+            }
+            this.omemoVerification.setOrEnsureEqual(omemoVerifiedPayload);
+            Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": received verifiable DTLS fingerprint via "+this.omemoVerification);
+            return omemoVerifiedPayload.getPayload();
+        } else if (expectVerification) {
+            throw new SecurityException("DTLS fingerprint was unexpectedly not verifiable");
+        } else {
+            return receivedContentMap;
+        }
+    }
+
     private void receiveSessionInitiate(final JinglePacket jinglePacket) {
         if (isInitiator()) {
             Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid()));
@@ -298,7 +321,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
         final RtpContentMap contentMap;
         try {
-            contentMap = RtpContentMap.of(jinglePacket);
+            contentMap = receiveRtpContentMap(jinglePacket, false);
             contentMap.requireContentDescriptions();
             contentMap.requireDTLSFingerprint();
         } catch (final RuntimeException e) {
@@ -328,6 +351,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
         if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
             respondOk(jinglePacket);
+            //TODO Do not push empty set
             pendingIceCandidates.push(contentMap.contents.entrySet());
             if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
                 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
@@ -350,7 +374,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
         final RtpContentMap contentMap;
         try {
-            contentMap = RtpContentMap.of(jinglePacket);
+            contentMap = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint());
             contentMap.requireContentDescriptions();
             contentMap.requireDTLSFingerprint();
         } catch (final RuntimeException e) {
@@ -469,7 +493,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     private void sendSessionAccept(final RtpContentMap rtpContentMap) {
         this.responderRtpContentMap = rtpContentMap;
         this.transitionOrThrow(State.SESSION_ACCEPTED);
-        final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
+        final RtpContentMap outgoingContentMap;
+        //TODO do on different thread
+        if (this.omemoVerification.hasDeviceId()) {
+            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": encrypting session-accept");
+            try {
+                final AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap> verifiedPayload = id.account.getAxolotlService().encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId());
+                outgoingContentMap = verifiedPayload.getPayload();
+                this.omemoVerification.setOrEnsureEqual(verifiedPayload);
+            } catch (final Exception e) {
+                //TODO fail application if something goes wrong here
+                Log.d(Config.LOGTAG, "unable to encrypt", e);
+                return;
+            }
+        } else {
+            outgoingContentMap = rtpContentMap;
+        }
+        final JinglePacket sessionAccept = outgoingContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
         send(sessionAccept);
     }
 
@@ -480,7 +520,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                 receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp);
                 break;
             case "proceed":
-                receiveProceed(from, serverMessageId, timestamp);
+                receiveProceed(from, Proceed.upgrade(message), serverMessageId, timestamp);
                 break;
             case "retract":
                 receiveRetract(from, serverMessageId, timestamp);
@@ -621,7 +661,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
     }
 
-    private void receiveProceed(final Jid from, final String serverMsgId, final long timestamp) {
+    private void receiveProceed(final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) {
         final Set<Media> media = Preconditions.checkNotNull(this.proposedMedia, "Proposed media has to be set before handling proceed");
         Preconditions.checkState(media.size() > 0, "Proposed media should not be empty");
         if (from.equals(id.with)) {
@@ -631,6 +671,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
                         this.message.setServerMsgId(serverMsgId);
                     }
                     this.message.setTime(timestamp);
+                    this.omemoVerification.setDeviceId(proceed.getDeviceId());
                     this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED);
                 } else {
                     Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state));
@@ -716,13 +757,31 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
     }
 
-    private void sendSessionInitiate(RtpContentMap rtpContentMap, final State targetState) {
+    private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) {
         this.initiatorRtpContentMap = rtpContentMap;
         this.transitionOrThrow(targetState);
-        final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
+        //TODO do on background thread?
+        final RtpContentMap outgoingContentMap = encryptSessionInitiate(rtpContentMap);
+        final JinglePacket sessionInitiate = outgoingContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
         send(sessionInitiate);
     }
 
+    private RtpContentMap encryptSessionInitiate(final RtpContentMap rtpContentMap) {
+        if (this.omemoVerification.hasDeviceId()) {
+            final AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap> verifiedPayload;
+            try {
+                verifiedPayload = id.account.getAxolotlService().encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId());
+            } catch (final CryptoFailedException e) {
+                Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e);
+                return rtpContentMap;
+            }
+            this.omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint());
+            return verifiedPayload.getPayload();
+        } else {
+            return rtpContentMap;
+        }
+    }
+
     private void sendSessionTerminate(final Reason reason) {
         sendSessionTerminate(reason, null);
     }
@@ -1055,12 +1114,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     private void sendJingleMessage(final String action, final Jid to) {
         final MessagePacket messagePacket = new MessagePacket();
+        messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
+        messagePacket.setTo(to);
+        final Element intent = messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId);
         if ("proceed".equals(action)) {
             messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId);
+
+            //TODO only do this if OMEMO is enable so we have an easy way to opt out
+            final int deviceId = id.account.getAxolotlService().getOwnDeviceId();
+            final Element device = intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION);
+            device.setAttribute("id", deviceId);
         }
-        messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
-        messagePacket.setTo(to);
-        messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId);
         messagePacket.addChild("store", "urn:xmpp:hints");
         xmppConnectionService.sendMessagePacket(id.account, messagePacket);
     }

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

@@ -0,0 +1,83 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+
+public class OmemoVerification {
+
+    private final AtomicBoolean deviceIdWritten = new AtomicBoolean(false);
+    private final AtomicBoolean sessionFingerprintWritten = new AtomicBoolean(false);
+    private Integer deviceId;
+    private String sessionFingerprint;
+
+    public void setDeviceId(final Integer id) {
+        if (deviceIdWritten.compareAndSet(false, true)) {
+            this.deviceId = id;
+            return;
+        }
+        throw new IllegalStateException("Device Id has already been set");
+    }
+
+    public int getDeviceId() {
+        Preconditions.checkNotNull(this.deviceId, "Device ID is null");
+        return this.deviceId;
+    }
+
+    public boolean hasDeviceId() {
+        return this.deviceId != null;
+    }
+
+    public void setSessionFingerprint(final String fingerprint) {
+        Preconditions.checkNotNull(fingerprint, "Session fingerprint must not be null");
+        if (sessionFingerprintWritten.compareAndSet(false, true)) {
+            this.sessionFingerprint = fingerprint;
+            return;
+        }
+        throw new IllegalStateException("Session fingerprint has already been set");
+    }
+
+    public String getFingerprint() {
+        return this.sessionFingerprint;
+    }
+
+    public void setOrEnsureEqual(AxolotlService.OmemoVerifiedPayload<?> omemoVerifiedPayload) {
+        setOrEnsureEqual(omemoVerifiedPayload.getDeviceId(), omemoVerifiedPayload.getFingerprint());
+    }
+
+    public void setOrEnsureEqual(final int deviceId, final String sessionFingerprint) {
+        Preconditions.checkNotNull(sessionFingerprint, "Session fingerprint must not be null");
+        if (this.deviceIdWritten.get() || this.sessionFingerprintWritten.get()) {
+            if (this.sessionFingerprint == null) {
+                throw new IllegalStateException("No session fingerprint has been previously provided");
+            }
+            if (!sessionFingerprint.equals(this.sessionFingerprint)) {
+                throw new IllegalStateException("Session Fingerprints did not match");
+            }
+            if (this.deviceId == null) {
+                throw new IllegalStateException("No Device Id has been previously provided");
+            }
+            if (this.deviceId != deviceId) {
+                throw new IllegalStateException("Device Ids did not match");
+            }
+        } else {
+            this.setSessionFingerprint(sessionFingerprint);
+            this.setDeviceId(deviceId);
+        }
+    }
+
+    public boolean hasFingerprint() {
+        return this.sessionFingerprint != null;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("deviceId", deviceId)
+                .add("fingerprint", sessionFingerprint)
+                .toString();
+    }
+}

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

@@ -0,0 +1,19 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import java.util.Map;
+
+import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
+import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
+
+public class OmemoVerifiedRtpContentMap extends RtpContentMap {
+    public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
+        super(group, contents);
+        for(final DescriptionTransport descriptionTransport : contents.values()) {
+            if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
+                ((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
+                continue;
+            }
+            throw new IllegalStateException("OmemoVerifiedRtpContentMap contains non-verified transport info");
+        }
+    }
+}

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

@@ -14,6 +14,7 @@ import com.google.common.collect.Sets;
 
 import org.checkerframework.checker.nullness.compatqual.NullableDecl;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -25,6 +26,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
 import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 
 public class RtpContentMap {
@@ -32,13 +34,32 @@ public class RtpContentMap {
     public final Group group;
     public final Map<String, DescriptionTransport> contents;
 
-    private RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
+    public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
         this.group = group;
         this.contents = contents;
     }
 
     public static RtpContentMap of(final JinglePacket jinglePacket) {
-        return new RtpContentMap(jinglePacket.getGroup(), DescriptionTransport.of(jinglePacket.getJingleContents()));
+        final Map<String, DescriptionTransport> contents =  DescriptionTransport.of(jinglePacket.getJingleContents());
+        if (isOmemoVerified(contents)) {
+            return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
+        } else {
+            return new RtpContentMap(jinglePacket.getGroup(), contents);
+        }
+    }
+
+    private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) {
+        final Collection<DescriptionTransport> values = contents.values();
+        if (values.size() == 0) {
+            return false;
+        }
+        for(final DescriptionTransport descriptionTransport : values) {
+            if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
+                continue;
+            }
+            return false;
+        }
+        return true;
     }
 
     public static RtpContentMap of(final SessionDescription sessionDescription) {
@@ -123,7 +144,7 @@ public class RtpContentMap {
         public final RtpDescription description;
         public final IceUdpTransportInfo transport;
 
-        DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
+        public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
             this.description = description;
             this.transport = transport;
         }
@@ -146,7 +167,10 @@ public class RtpContentMap {
             } else {
                 throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
             }
-            return new DescriptionTransport(rtpDescription, iceUdpTransportInfo);
+            return new DescriptionTransport(
+                    rtpDescription,
+                    OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)
+            );
         }
 
         public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) {

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/OmemoVerifiedIceUdpTransportInfo.java 🔗

@@ -0,0 +1,27 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import eu.siacs.conversations.xml.Namespace;
+
+public class OmemoVerifiedIceUdpTransportInfo extends IceUdpTransportInfo {
+
+
+    public void ensureNoPlaintextFingerprint() {
+        if (this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS) != null) {
+            throw new IllegalStateException("OmemoVerifiedIceUdpTransportInfo contains plaintext fingerprint");
+        }
+    }
+
+    public static IceUdpTransportInfo upgrade(final IceUdpTransportInfo transportInfo) {
+        if (transportInfo.hasChild("fingerprint", Namespace.JINGLE_APPS_DTLS)) {
+            return transportInfo;
+        }
+        if (transportInfo.hasChild("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION)) {
+            final OmemoVerifiedIceUdpTransportInfo omemoVerifiedIceUdpTransportInfo = new OmemoVerifiedIceUdpTransportInfo();
+            omemoVerifiedIceUdpTransportInfo.setAttributes(transportInfo.getAttributes());
+            omemoVerifiedIceUdpTransportInfo.setChildren(transportInfo.getChildren());
+            return omemoVerifiedIceUdpTransportInfo;
+        }
+        return transportInfo;
+    }
+
+}

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java 🔗

@@ -0,0 +1,34 @@
+package eu.siacs.conversations.xmpp.jingle.stanzas;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Ints;
+
+import java.util.List;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+
+public class Proceed extends Element {
+    private Proceed() {
+        super("propose", Namespace.JINGLE_MESSAGE);
+    }
+
+    public static Proceed upgrade(final Element element) {
+        Preconditions.checkArgument("proceed".equals(element.getName()));
+        Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(element.getNamespace()));
+        final Proceed propose = new Proceed();
+        propose.setAttributes(element.getAttributes());
+        propose.setChildren(element.getChildren());
+        return propose;
+    }
+
+    public Integer getDeviceId() {
+        final Element device = this.findChild("device");
+        final String id = device == null ? null : device.getAttribute("id");
+        if (id == null) {
+            return null;
+        }
+        return Ints.tryParse(id);
+    }
+}