encrypt rtp map as future

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java   |  45 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 152 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java      |   4 
3 files changed, 141 insertions(+), 60 deletions(-)

Detailed changes

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

@@ -8,7 +8,6 @@ import android.util.Pair;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.Futures;
@@ -1238,31 +1237,51 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
     }
 
 
-    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);
-        if (session == null) {
-            throw new CryptoFailedException(String.format("No session found for %d", deviceId));
-        }
+    public ListenableFuture<OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) {
+        return Futures.transformAsync(
+                getSession(jid, deviceId),
+                session -> encrypt(rtpContentMap, session),
+                MoreExecutors.directExecutor()
+        );
+    }
+
+    private ListenableFuture<OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) {
         if (Config.REQUIRE_RTP_VERIFICATION) {
             requireVerification(session);
         }
         final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
         final OmemoVerification omemoVerification = new OmemoVerification();
-        omemoVerification.setDeviceId(deviceId);
+        omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
         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);
+            final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
+            try {
+                encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
+            } catch (final CryptoFailedException e) {
+                return Futures.immediateFailedFuture(e);
+            }
             descriptionTransportBuilder.put(
                     content.getKey(),
                     new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo)
             );
         }
-        return new OmemoVerifiedPayload<>(
-                omemoVerification,
-                new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build())
-        );
+        return Futures.immediateFuture(
+                new OmemoVerifiedPayload<>(
+                        omemoVerification,
+                        new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build())
+                ));
+    }
+
+    private ListenableFuture<XmppAxolotlSession> getSession(final Jid jid, final int deviceId) {
+        final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
+        final XmppAxolotlSession session = sessions.get(address);
+        if (session == null) {
+            return Futures.immediateFailedFuture(
+                    new CryptoFailedException(String.format("No session found for %d", deviceId))
+            );
+        }
+        return Futures.immediateFuture(session);
     }
 
     public ListenableFuture<OmemoVerifiedPayload<RtpContentMap>> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) {

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

@@ -318,6 +318,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) {
             final ListenableFuture<AxolotlService.OmemoVerifiedPayload<RtpContentMap>> future = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with);
             return Futures.transform(future, omemoVerifiedPayload -> {
+                //TODO test if an exception here triggers a correct abort
                 omemoVerification.setOrEnsureEqual(omemoVerifiedPayload);
                 Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + omemoVerification);
                 return omemoVerifiedPayload.getPayload();
@@ -532,17 +533,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
             this.webRTCWrapper.setRemoteDescription(sdp).get();
             addIceCandidatesFromBlackLog();
             org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get();
-            final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
-            final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
-            sendSessionAccept(respondingRtpContentMap);
-            this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
+            prepareSessionAccept(webRTCSessionDescription);
         } catch (final Exception e) {
-            Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(e));
-            webRTCWrapper.close();
-            sendSessionTerminate(Reason.FAILED_APPLICATION);
+            failureToAcceptSession(e);
         }
     }
 
+    private void failureToAcceptSession(final Throwable throwable) {
+        Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(throwable));
+        webRTCWrapper.close();
+        sendSessionTerminate(Reason.ofThrowable(throwable));
+    }
+
     private void addIceCandidatesFromBlackLog() {
         while (!this.pendingIceCandidates.isEmpty()) {
             processCandidates(this.pendingIceCandidates.poll());
@@ -550,24 +552,49 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
     }
 
-    private void sendSessionAccept(final RtpContentMap rtpContentMap) {
-        this.responderRtpContentMap = rtpContentMap;
-        this.transitionOrThrow(State.SESSION_ACCEPTED);
-        final RtpContentMap outgoingContentMap;
+    private void prepareSessionAccept(final org.webrtc.SessionDescription webRTCSessionDescription) {
+        final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
+        final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
+        this.responderRtpContentMap = respondingRtpContentMap;
+        final ListenableFuture<RtpContentMap> outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap);
+        Futures.addCallback(outgoingContentMapFuture,
+                new FutureCallback<RtpContentMap>() {
+                    @Override
+                    public void onSuccess(final RtpContentMap outgoingContentMap) {
+                        sendSessionAccept(outgoingContentMap, webRTCSessionDescription);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable throwable) {
+                        failureToAcceptSession(throwable);
+                    }
+                },
+                MoreExecutors.directExecutor()
+        );
+    }
+
+    private void sendSessionAccept(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription) {
+        transitionOrThrow(State.SESSION_ACCEPTED);
+        final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
+        send(sessionAccept);
+        try {
+            webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
+        } catch (Exception e) {
+            failureToAcceptSession(e);
+        }
+    }
+
+    private ListenableFuture<RtpContentMap> prepareOutgoingContentMap(final RtpContentMap rtpContentMap) {
         if (this.omemoVerification.hasDeviceId()) {
-            final AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap> verifiedPayload;
-            try {
-                verifiedPayload = id.account.getAxolotlService().encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId());
-                outgoingContentMap = verifiedPayload.getPayload();
-                this.omemoVerification.setOrEnsureEqual(verifiedPayload);
-            } catch (final Exception e) {
-                throw new SecurityException("Unable to verify DTLS Fingerprint with OMEMO", e);
-            }
+            ListenableFuture<AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>> verifiedPayloadFuture = id.account.getAxolotlService()
+                    .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId());
+            return Futures.transform(verifiedPayloadFuture, verifiedPayload -> {
+                omemoVerification.setOrEnsureEqual(verifiedPayload);
+                return verifiedPayload.getPayload();
+            }, MoreExecutors.directExecutor());
         } else {
-            outgoingContentMap = rtpContentMap;
+            return Futures.immediateFuture(rtpContentMap);
         }
-        final JinglePacket sessionAccept = outgoingContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
-        send(sessionAccept);
     }
 
     synchronized void deliveryMessage(final Jid from, final Element message, final String serverMessageId, final long timestamp) {
@@ -803,19 +830,20 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         }
         try {
             org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get();
-            final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
-            final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
-            sendSessionInitiate(rtpContentMap, targetState);
-            this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
+            prepareSessionInitiate(webRTCSessionDescription, targetState);
         } catch (final Exception e) {
-            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(e));
-            webRTCWrapper.close();
-            final Reason reason = Reason.ofThrowable(e);
-            if (isInState(targetState)) {
-                sendSessionTerminate(reason);
-            } else {
-                sendRetract(reason);
-            }
+            failureToInitiateSession(e, targetState);
+        }
+    }
+
+    private void failureToInitiateSession(final Throwable throwable, final State targetState) {
+        Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(throwable));
+        webRTCWrapper.close();
+        final Reason reason = Reason.ofThrowable(throwable);
+        if (isInState(targetState)) {
+            sendSessionTerminate(reason);
+        } else {
+            sendRetract(reason);
         }
     }
 
@@ -826,27 +854,57 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
         this.finish();
     }
 
-    private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) {
+    private void prepareSessionInitiate(final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) {
+        final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
+        final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription);
         this.initiatorRtpContentMap = rtpContentMap;
-        final RtpContentMap outgoingContentMap = encryptSessionInitiate(rtpContentMap);
-        final JinglePacket sessionInitiate = outgoingContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
+        final ListenableFuture<RtpContentMap> outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap);
+        Futures.addCallback(outgoingContentMapFuture, new FutureCallback<RtpContentMap>() {
+            @Override
+            public void onSuccess(final RtpContentMap outgoingContentMap) {
+                sendSessionInitiate(outgoingContentMap, webRTCSessionDescription, targetState);
+            }
+
+            @Override
+            public void onFailure(@NonNull final Throwable throwable) {
+                failureToInitiateSession(throwable, targetState);
+            }
+        }, MoreExecutors.directExecutor());
+    }
+
+    private void sendSessionInitiate(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) {
         this.transitionOrThrow(targetState);
+        final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
         send(sessionInitiate);
+        try {
+            this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
+        } catch (Exception e) {
+            failureToInitiateSession(e, targetState);
+        }
     }
 
-    private RtpContentMap encryptSessionInitiate(final RtpContentMap rtpContentMap) {
+    private ListenableFuture<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;
+            final ListenableFuture<AxolotlService.OmemoVerifiedPayload<OmemoVerifiedRtpContentMap>> verifiedPayloadFuture = id.account.getAxolotlService()
+                    .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId());
+            final ListenableFuture<RtpContentMap> future = Futures.transform(verifiedPayloadFuture, verifiedPayload -> {
+                omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint());
+                return verifiedPayload.getPayload();
+            }, MoreExecutors.directExecutor());
+            if (Config.REQUIRE_RTP_VERIFICATION) {
+                return future;
             }
-            this.omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint());
-            return verifiedPayload.getPayload();
+            return Futures.catching(
+                    future,
+                    CryptoFailedException.class,
+                    e -> {
+                        Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e);
+                        return rtpContentMap;
+                    },
+                    MoreExecutors.directExecutor()
+            );
         } else {
-            return rtpContentMap;
+            return Futures.immediateFuture(rtpContentMap);
         }
     }
 

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

@@ -6,6 +6,7 @@ import com.google.common.base.CaseFormat;
 import com.google.common.base.Throwables;
 
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
 import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
 
 public enum Reason {
@@ -59,6 +60,9 @@ public enum Reason {
         if (root instanceof RuntimeException) {
             return of((RuntimeException) root);
         }
+        if (root instanceof CryptoFailedException) {
+            return SECURITY_ERROR;
+        }
         return FAILED_APPLICATION;
     }
 }