Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/iNPUTmice/Conversations.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Gultsch <daniel@gultsch.de>2021-03-02 23:13:49 +0300
committerDaniel Gultsch <daniel@gultsch.de>2021-03-02 23:13:49 +0300
commit8a6430ae29a50b0d71ea79884eaac4fe7841bb7d (patch)
treeee82ca94f886ee00f7b556ddbc343e58811f03e4
parent47a904b4fc0d6644b53fdd41bebd0211b6e32b4a (diff)
ground work for omemo dtls verification
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java119
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java4
-rw-r--r--src/main/java/eu/siacs/conversations/xml/Namespace.java1
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java84
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerification.java83
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java19
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java32
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java2
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/OmemoVerifiedIceUdpTransportInfo.java27
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Proceed.java34
10 files changed, 387 insertions, 18 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 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<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;
+ }
+ }
}
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<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);
}
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<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");
+ }
+ }
+}
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<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) {
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);
+ }
+}