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 b43d331c8..57b0a0d43 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/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 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 descriptionTransportBuilder = new ImmutableMap.Builder<>(); + final OmemoVerification omemoVerification = new OmemoVerification(); + omemoVerification.setDeviceId(deviceId); + 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); + descriptionTransportBuilder.put( + content.getKey(), + new RtpContentMap.DescriptionTransport(descriptionTransport.description, encryptedTransportInfo) + ); + } + return new OmemoVerifiedPayload<>( + omemoVerification, + new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build()) + ); + } + + public OmemoVerifiedPayload decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) throws CryptoFailedException { + final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>(); + final OmemoVerification omemoVerification = new OmemoVerification(); + for (final Map.Entry content : omemoVerifiedRtpContentMap.contents.entrySet()) { + final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); + final OmemoVerifiedPayload 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 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 { + 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; + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 1c9db15f9..bba24d90b 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/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); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index b65076016..29b4bf395 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/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"; } 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 572c9f766..5d00f0c00 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/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>> 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 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 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 = 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 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(); - if ("proceed".equals(action)) { - messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId); - } messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those messagePacket.setTo(to); - messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); + 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.addChild("store", "urn:xmpp:hints"); xmppConnectionService.sendMessagePacket(id.account, messagePacket); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerification.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerification.java new file mode 100644 index 000000000..0be0f2cf7 --- /dev/null +++ b/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(); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java new file mode 100644 index 000000000..f5e041014 --- /dev/null +++ b/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 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"); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 11ad513b7..38935d8fb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/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 contents; - private RtpContentMap(Group group, Map contents) { + public RtpContentMap(Group group, Map 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 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 contents) { + final Collection 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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index e43556d17..022c4d2dd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -22,7 +22,7 @@ import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class IceUdpTransportInfo extends GenericTransportInfo { - private IceUdpTransportInfo() { + public IceUdpTransportInfo() { super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/OmemoVerifiedIceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/OmemoVerifiedIceUdpTransportInfo.java new file mode 100644 index 000000000..d59dbbb9a --- /dev/null +++ b/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; + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java new file mode 100644 index 000000000..a9c399754 --- /dev/null +++ b/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); + } +}