ground work for omemo dtls verification
This commit is contained in:
parent
47a904b4fc
commit
8a6430ae29
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue