From 8d391753d74a441099b3b2372f3cacc35cb382d1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 8 May 2021 08:45:31 +0200 Subject: [PATCH] encrypt rtp map as future --- .../crypto/axolotl/AxolotlService.java | 45 +++-- .../xmpp/jingle/JingleRtpConnection.java | 160 ++++++++++++------ .../xmpp/jingle/stanzas/Reason.java | 4 + 3 files changed, 145 insertions(+), 64 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 81bfb8a46..afd3bf6f3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/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 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> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { + return Futures.transformAsync( + getSession(jid, deviceId), + session -> encrypt(rtpContentMap, session), + MoreExecutors.directExecutor() + ); + } + + private ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { if (Config.REQUIRE_RTP_VERIFICATION) { requireVerification(session); } final ImmutableMap.Builder 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 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 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> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index c933439e7..0d147d2af 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/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> 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; - if (this.omemoVerification.hasDeviceId()) { - final AxolotlService.OmemoVerifiedPayload 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); - } - } else { - outgoingContentMap = rtpContentMap; - } - final JinglePacket sessionAccept = outgoingContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + 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 outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); + Futures.addCallback(outgoingContentMapFuture, + new FutureCallback() { + @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 prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { + if (this.omemoVerification.hasDeviceId()) { + ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() + .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); + return Futures.transform(verifiedPayloadFuture, verifiedPayload -> { + omemoVerification.setOrEnsureEqual(verifiedPayload); + return verifiedPayload.getPayload(); + }, MoreExecutors.directExecutor()); + } else { + return Futures.immediateFuture(rtpContentMap); + } } 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); - this.transitionOrThrow(targetState); - send(sessionInitiate); + final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); + Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { + @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 RtpContentMap encryptSessionInitiate(final RtpContentMap rtpContentMap) { + 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 ListenableFuture encryptSessionInitiate(final RtpContentMap rtpContentMap) { if (this.omemoVerification.hasDeviceId()) { - final AxolotlService.OmemoVerifiedPayload 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> verifiedPayloadFuture = id.account.getAxolotlService() + .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); + final ListenableFuture 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); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 0d6e60fde..669a01c49 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/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; } } \ No newline at end of file