ground work for omemo dtls verification

This commit is contained in:
Daniel Gultsch 2021-03-02 21:13:49 +01:00
parent 47a904b4fc
commit 8a6430ae29
10 changed files with 388 additions and 19 deletions

View File

@ -8,6 +8,8 @@ import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.common.collect.ImmutableMap;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair; 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.CryptoHelper;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
import eu.siacs.conversations.xmpp.OnIqPacketReceived; 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.pep.PublishOptions;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket; 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) { public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
@ -1267,7 +1360,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} catch (final BrokenSessionException e) { } catch (final BrokenSessionException e) {
throw e; throw e;
} catch (final OutdatedSenderException 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; throw e;
} catch (CryptoFailedException e) { } catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), 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;
}
}
} }

View File

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

View File

@ -53,4 +53,5 @@ public final class Namespace {
public static final String INVITE = "urn:xmpp:invite"; public static final String INVITE = "urn:xmpp:invite";
public static final String PARS = "urn:xmpp:pars:0"; public static final String PARS = "urn:xmpp:pars:0";
public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite"; 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";
} }

View File

@ -30,6 +30,8 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import eu.siacs.conversations.Config; 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.Account;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational; 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.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; 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.Propose;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; 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 WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
private final ArrayDeque<Set<Map.Entry<String, RtpContentMap.DescriptionTransport>>> pendingIceCandidates = new ArrayDeque<>(); private final ArrayDeque<Set<Map.Entry<String, RtpContentMap.DescriptionTransport>>> pendingIceCandidates = new ArrayDeque<>();
private final OmemoVerification omemoVerification = new OmemoVerification();
private final Message message; private final Message message;
private State state = State.NULL; private State state = State.NULL;
private StateTransitionException stateTransitionException; 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) { private void receiveSessionInitiate(final JinglePacket jinglePacket) {
if (isInitiator()) { if (isInitiator()) {
Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); 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; final RtpContentMap contentMap;
try { try {
contentMap = RtpContentMap.of(jinglePacket); contentMap = receiveRtpContentMap(jinglePacket, false);
contentMap.requireContentDescriptions(); contentMap.requireContentDescriptions();
contentMap.requireDTLSFingerprint(); contentMap.requireDTLSFingerprint();
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
@ -328,6 +351,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
respondOk(jinglePacket); respondOk(jinglePacket);
//TODO Do not push empty set
pendingIceCandidates.push(contentMap.contents.entrySet()); pendingIceCandidates.push(contentMap.contents.entrySet());
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); 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; final RtpContentMap contentMap;
try { try {
contentMap = RtpContentMap.of(jinglePacket); contentMap = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint());
contentMap.requireContentDescriptions(); contentMap.requireContentDescriptions();
contentMap.requireDTLSFingerprint(); contentMap.requireDTLSFingerprint();
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
@ -469,7 +493,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void sendSessionAccept(final RtpContentMap rtpContentMap) { private void sendSessionAccept(final RtpContentMap rtpContentMap) {
this.responderRtpContentMap = rtpContentMap; this.responderRtpContentMap = rtpContentMap;
this.transitionOrThrow(State.SESSION_ACCEPTED); 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); send(sessionAccept);
} }
@ -480,7 +520,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp); receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp);
break; break;
case "proceed": case "proceed":
receiveProceed(from, serverMessageId, timestamp); receiveProceed(from, Proceed.upgrade(message), serverMessageId, timestamp);
break; break;
case "retract": case "retract":
receiveRetract(from, serverMessageId, timestamp); 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"); 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"); Preconditions.checkState(media.size() > 0, "Proposed media should not be empty");
if (from.equals(id.with)) { if (from.equals(id.with)) {
@ -631,6 +671,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
this.message.setServerMsgId(serverMsgId); this.message.setServerMsgId(serverMsgId);
} }
this.message.setTime(timestamp); this.message.setTime(timestamp);
this.omemoVerification.setDeviceId(proceed.getDeviceId());
this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED); this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED);
} else { } else {
Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); 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.initiatorRtpContentMap = rtpContentMap;
this.transitionOrThrow(targetState); 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); 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) { private void sendSessionTerminate(final Reason reason) {
sendSessionTerminate(reason, null); sendSessionTerminate(reason, null);
} }
@ -1055,12 +1114,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
private void sendJingleMessage(final String action, final Jid to) { private void sendJingleMessage(final String action, final Jid to) {
final MessagePacket messagePacket = new MessagePacket(); 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.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
messagePacket.setTo(to); 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"); messagePacket.addChild("store", "urn:xmpp:hints");
xmppConnectionService.sendMessagePacket(id.account, messagePacket); xmppConnectionService.sendMessagePacket(id.account, messagePacket);
} }

View File

@ -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();
}
}

View File

@ -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");
}
}
}

View File

@ -14,6 +14,7 @@ import com.google.common.collect.Sets;
import org.checkerframework.checker.nullness.compatqual.NullableDecl; import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public class RtpContentMap { public class RtpContentMap {
@ -32,13 +34,32 @@ public class RtpContentMap {
public final Group group; public final Group group;
public final Map<String, DescriptionTransport> contents; 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.group = group;
this.contents = contents; this.contents = contents;
} }
public static RtpContentMap of(final JinglePacket jinglePacket) { 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) { public static RtpContentMap of(final SessionDescription sessionDescription) {
@ -123,7 +144,7 @@ public class RtpContentMap {
public final RtpDescription description; public final RtpDescription description;
public final IceUdpTransportInfo transport; public final IceUdpTransportInfo transport;
DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
this.description = description; this.description = description;
this.transport = transport; this.transport = transport;
} }
@ -146,7 +167,10 @@ public class RtpContentMap {
} else { } else {
throw new UnsupportedTransportException("Content does not contain ICE-UDP transport"); 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) { public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) {

View File

@ -22,7 +22,7 @@ import eu.siacs.conversations.xmpp.jingle.SessionDescription;
public class IceUdpTransportInfo extends GenericTransportInfo { public class IceUdpTransportInfo extends GenericTransportInfo {
private IceUdpTransportInfo() { public IceUdpTransportInfo() {
super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP);
} }

View File

@ -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;
}
}

View File

@ -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);
}
}