1.0.1 #24
|
@ -71,6 +71,7 @@ dependencies {
|
||||||
implementation 'com.google.guava:guava:32.1.3-android'
|
implementation 'com.google.guava:guava:32.1.3-android'
|
||||||
implementation 'io.michaelrocks:libphonenumber-android:8.13.17'
|
implementation 'io.michaelrocks:libphonenumber-android:8.13.17'
|
||||||
implementation 'im.conversations.webrtc:webrtc-android:119.0.0'
|
implementation 'im.conversations.webrtc:webrtc-android:119.0.0'
|
||||||
|
implementation 'org.jitsi:org.otr4j:0.23'
|
||||||
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
|
|
|
@ -299,6 +299,11 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.PublishGroupChatProfilePictureActivity"
|
android:name=".ui.PublishGroupChatProfilePictureActivity"
|
||||||
android:label="@string/group_chat_avatar" />
|
android:label="@string/group_chat_avatar" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.VerifyOTRActivity"
|
||||||
|
android:label="@string/verify_otr"
|
||||||
|
android:exported="false"
|
||||||
|
android:windowSoftInputMode="stateHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ShareWithActivity"
|
android:name=".ui.ShareWithActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
|
@ -15,9 +15,10 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
public final class Config {
|
public final class Config {
|
||||||
private static final int UNENCRYPTED = 1;
|
private static final int UNENCRYPTED = 1;
|
||||||
private static final int OPENPGP = 2;
|
private static final int OPENPGP = 2;
|
||||||
|
private static final int OTR = 4;
|
||||||
private static final int OMEMO = 8;
|
private static final int OMEMO = 8;
|
||||||
|
|
||||||
private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OMEMO;
|
private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO;
|
||||||
|
|
||||||
public static boolean supportUnencrypted() {
|
public static boolean supportUnencrypted() {
|
||||||
return (ENCRYPTION_MASK & UNENCRYPTED) != 0;
|
return (ENCRYPTION_MASK & UNENCRYPTED) != 0;
|
||||||
|
@ -31,6 +32,10 @@ public final class Config {
|
||||||
return (ENCRYPTION_MASK & OMEMO) != 0;
|
return (ENCRYPTION_MASK & OMEMO) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean supportOtr() {
|
||||||
|
return (ENCRYPTION_MASK & OTR) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean omemoOnly() {
|
public static boolean omemoOnly() {
|
||||||
return !multipleEncryptionChoices() && supportOmemo();
|
return !multipleEncryptionChoices() && supportOmemo();
|
||||||
}
|
}
|
||||||
|
|
312
src/main/java/eu/siacs/conversations/crypto/OtrService.java
Normal file
312
src/main/java/eu/siacs/conversations/crypto/OtrService.java
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
package eu.siacs.conversations.crypto;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import net.java.otr4j.OtrEngineHost;
|
||||||
|
import net.java.otr4j.OtrException;
|
||||||
|
import net.java.otr4j.OtrPolicy;
|
||||||
|
import net.java.otr4j.OtrPolicyImpl;
|
||||||
|
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
|
||||||
|
import net.java.otr4j.crypto.OtrCryptoException;
|
||||||
|
import net.java.otr4j.session.FragmenterInstructions;
|
||||||
|
import net.java.otr4j.session.InstanceTag;
|
||||||
|
import net.java.otr4j.session.SessionID;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.DSAPrivateKeySpec;
|
||||||
|
import java.security.spec.DSAPublicKeySpec;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
|
import eu.siacs.conversations.generator.MessageGenerator;
|
||||||
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
|
import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
|
||||||
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
|
||||||
|
public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
|
||||||
|
|
||||||
|
private Account account;
|
||||||
|
private OtrPolicy otrPolicy;
|
||||||
|
private KeyPair keyPair;
|
||||||
|
private XmppConnectionService mXmppConnectionService;
|
||||||
|
|
||||||
|
public OtrService(XmppConnectionService service, Account account) {
|
||||||
|
this.account = account;
|
||||||
|
this.otrPolicy = new OtrPolicyImpl();
|
||||||
|
this.otrPolicy.setAllowV1(false);
|
||||||
|
this.otrPolicy.setAllowV2(true);
|
||||||
|
this.otrPolicy.setAllowV3(true);
|
||||||
|
this.keyPair = loadKey(account.getKeys());
|
||||||
|
this.mXmppConnectionService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyPair loadKey(final JSONObject keys) {
|
||||||
|
if (keys == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
synchronized (keys) {
|
||||||
|
try {
|
||||||
|
BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
|
||||||
|
BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
|
||||||
|
BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
|
||||||
|
BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
|
||||||
|
BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
|
||||||
|
DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
|
||||||
|
DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
|
||||||
|
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
|
||||||
|
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||||
|
return new KeyPair(publicKey, privateKey);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return null;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
return null;
|
||||||
|
} catch (InvalidKeySpecException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveKey() {
|
||||||
|
PublicKey publicKey = keyPair.getPublic();
|
||||||
|
PrivateKey privateKey = keyPair.getPrivate();
|
||||||
|
KeyFactory keyFactory;
|
||||||
|
try {
|
||||||
|
keyFactory = KeyFactory.getInstance("DSA");
|
||||||
|
DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(
|
||||||
|
privateKey, DSAPrivateKeySpec.class);
|
||||||
|
DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey,
|
||||||
|
DSAPublicKeySpec.class);
|
||||||
|
this.account.setKey("otr_x", privateKeySpec.getX().toString(16));
|
||||||
|
this.account.setKey("otr_g", privateKeySpec.getG().toString(16));
|
||||||
|
this.account.setKey("otr_p", privateKeySpec.getP().toString(16));
|
||||||
|
this.account.setKey("otr_q", privateKeySpec.getQ().toString(16));
|
||||||
|
this.account.setKey("otr_y", publicKeySpec.getY().toString(16));
|
||||||
|
} catch (final NoSuchAlgorithmException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (final InvalidKeySpecException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void askForSecret(SessionID id, InstanceTag instanceTag, String question) {
|
||||||
|
try {
|
||||||
|
final Jid jid = OtrJidHelper.fromSessionID(id);
|
||||||
|
Conversation conversation = this.mXmppConnectionService.find(this.account, jid, null);
|
||||||
|
if (conversation != null) {
|
||||||
|
conversation.smp().hint = question;
|
||||||
|
conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED;
|
||||||
|
mXmppConnectionService.updateConversationUi();
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": smp in invalid session " + id.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finishedSessionMessage(SessionID arg0, String arg1)
|
||||||
|
throws OtrException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFallbackMessage(SessionID arg0) {
|
||||||
|
return MessageGenerator.OTR_FALLBACK_MESSAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getLocalFingerprintRaw(SessionID arg0) {
|
||||||
|
try {
|
||||||
|
return getFingerprintRaw(getPublicKey());
|
||||||
|
} catch (OtrCryptoException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicKey getPublicKey() {
|
||||||
|
if (this.keyPair == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.keyPair.getPublic();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException {
|
||||||
|
if (this.keyPair == null) {
|
||||||
|
KeyPairGenerator kg;
|
||||||
|
try {
|
||||||
|
kg = KeyPairGenerator.getInstance("DSA");
|
||||||
|
this.keyPair = kg.genKeyPair();
|
||||||
|
this.saveKey();
|
||||||
|
mXmppConnectionService.databaseBackend.updateAccount(account);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
Log.d(Config.LOGTAG,
|
||||||
|
"error generating key pair " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.keyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getReplyForUnreadableMessage(SessionID arg0) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OtrPolicy getSessionPolicy(SessionID arg0) {
|
||||||
|
return otrPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectMessage(SessionID session, String body)
|
||||||
|
throws OtrException {
|
||||||
|
MessagePacket packet = new MessagePacket();
|
||||||
|
packet.setFrom(account.getJid());
|
||||||
|
if (session.getUserID().isEmpty()) {
|
||||||
|
packet.setAttribute("to", session.getAccountID());
|
||||||
|
} else {
|
||||||
|
packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID());
|
||||||
|
}
|
||||||
|
packet.setBody(body);
|
||||||
|
MessageGenerator.addMessageHints(packet);
|
||||||
|
try {
|
||||||
|
Jid jid = OtrJidHelper.fromSessionID(session);
|
||||||
|
Conversation conversation = mXmppConnectionService.find(account, jid, null);
|
||||||
|
if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
|
||||||
|
if (mXmppConnectionService.sendChatStates()) {
|
||||||
|
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final IllegalArgumentException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.setType(MessagePacket.TYPE_CHAT);
|
||||||
|
packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0");
|
||||||
|
account.getXmppConnection().sendMessagePacket(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void messageFromAnotherInstanceReceived(SessionID session) {
|
||||||
|
sendOtrErrorMessage(session, "Message from another OTR-instance received");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void multipleInstancesDetected(SessionID arg0) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requireEncryptedMessage(SessionID arg0, String arg1)
|
||||||
|
throws OtrException {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showError(SessionID arg0, String arg1) throws OtrException {
|
||||||
|
Log.d(Config.LOGTAG, "show error");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void smpAborted(SessionID id) throws OtrException {
|
||||||
|
setSmpStatus(id, Conversation.Smp.STATUS_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSmpStatus(SessionID id, int status) {
|
||||||
|
try {
|
||||||
|
final Jid jid = OtrJidHelper.fromSessionID(id);
|
||||||
|
Conversation conversation = this.mXmppConnectionService.find(this.account, jid, null);
|
||||||
|
if (conversation != null) {
|
||||||
|
conversation.smp().status = status;
|
||||||
|
mXmppConnectionService.updateConversationUi();
|
||||||
|
}
|
||||||
|
} catch (final IllegalArgumentException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void smpError(SessionID id, int arg1, boolean arg2)
|
||||||
|
throws OtrException {
|
||||||
|
setSmpStatus(id, Conversation.Smp.STATUS_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unencryptedMessageReceived(SessionID arg0, String arg1)
|
||||||
|
throws OtrException {
|
||||||
|
throw new OtrException(new Exception("unencrypted message received"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unreadableMessageReceived(SessionID session) throws OtrException {
|
||||||
|
Log.d(Config.LOGTAG, "unreadable message received");
|
||||||
|
sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendOtrErrorMessage(SessionID session, String errorText) {
|
||||||
|
try {
|
||||||
|
Jid jid = OtrJidHelper.fromSessionID(session);
|
||||||
|
Conversation conversation = mXmppConnectionService.find(account, jid, null);
|
||||||
|
String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId();
|
||||||
|
if (id != null) {
|
||||||
|
MessagePacket packet = mXmppConnectionService.getMessageGenerator()
|
||||||
|
.generateOtrError(jid, id, errorText);
|
||||||
|
packet.setFrom(account.getJid());
|
||||||
|
mXmppConnectionService.sendMessagePacket(account, packet);
|
||||||
|
Log.d(Config.LOGTAG, packet.toString());
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
|
||||||
|
+ ": unreadable OTR message in " + conversation.getName());
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unverify(SessionID id, String arg1) {
|
||||||
|
setSmpStatus(id, Conversation.Smp.STATUS_FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(SessionID id, String fingerprint, boolean approved) {
|
||||||
|
Log.d(Config.LOGTAG, "OtrService.verify(" + id.toString() + "," + fingerprint + "," + String.valueOf(approved) + ")");
|
||||||
|
try {
|
||||||
|
final Jid jid = OtrJidHelper.fromSessionID(id);
|
||||||
|
Conversation conversation = this.mXmppConnectionService.find(this.account, jid, null);
|
||||||
|
if (conversation != null) {
|
||||||
|
if (approved) {
|
||||||
|
conversation.getContact().addOtrFingerprint(fingerprint);
|
||||||
|
}
|
||||||
|
conversation.smp().hint = null;
|
||||||
|
conversation.smp().status = Conversation.Smp.STATUS_VERIFIED;
|
||||||
|
mXmppConnectionService.updateConversationUi();
|
||||||
|
mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
|
||||||
|
}
|
||||||
|
} catch (final IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,20 +8,27 @@ import android.util.Log;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
|
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
|
||||||
|
import net.java.otr4j.crypto.OtrCryptoException;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.interfaces.DSAPublicKey;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
|
import eu.siacs.conversations.crypto.OtrService;
|
||||||
import eu.siacs.conversations.crypto.PgpDecryptionService;
|
import eu.siacs.conversations.crypto.PgpDecryptionService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
||||||
|
@ -93,6 +100,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
protected String avatar;
|
protected String avatar;
|
||||||
protected String hostname = null;
|
protected String hostname = null;
|
||||||
protected int port = 5222;
|
protected int port = 5222;
|
||||||
|
private OtrService mOtrService = null;
|
||||||
protected boolean online = false;
|
protected boolean online = false;
|
||||||
private String rosterVersion;
|
private String rosterVersion;
|
||||||
private String displayName = null;
|
private String displayName = null;
|
||||||
|
@ -100,6 +108,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
private PgpDecryptionService pgpDecryptionService = null;
|
private PgpDecryptionService pgpDecryptionService = null;
|
||||||
private XmppConnection xmppConnection = null;
|
private XmppConnection xmppConnection = null;
|
||||||
private long mEndGracePeriod = 0L;
|
private long mEndGracePeriod = 0L;
|
||||||
|
private String otrFingerprint;
|
||||||
private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
|
private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
|
||||||
private Presence.Status presenceStatus;
|
private Presence.Status presenceStatus;
|
||||||
private String presenceStatusMessage;
|
private String presenceStatusMessage;
|
||||||
|
@ -535,6 +544,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initAccountServices(final XmppConnectionService context) {
|
public void initAccountServices(final XmppConnectionService context) {
|
||||||
|
this.mOtrService = new OtrService(context, this);
|
||||||
this.axolotlService = new AxolotlService(this, context);
|
this.axolotlService = new AxolotlService(this, context);
|
||||||
this.pgpDecryptionService = new PgpDecryptionService(context);
|
this.pgpDecryptionService = new PgpDecryptionService(context);
|
||||||
if (xmppConnection != null) {
|
if (xmppConnection != null) {
|
||||||
|
@ -542,6 +552,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OtrService getOtrService() {
|
||||||
|
return this.mOtrService;
|
||||||
|
}
|
||||||
|
|
||||||
public PgpDecryptionService getPgpDecryptionService() {
|
public PgpDecryptionService getPgpDecryptionService() {
|
||||||
return this.pgpDecryptionService;
|
return this.pgpDecryptionService;
|
||||||
}
|
}
|
||||||
|
@ -554,6 +568,27 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
this.xmppConnection = connection;
|
this.xmppConnection = connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOtrFingerprint() {
|
||||||
|
if (this.otrFingerprint == null) {
|
||||||
|
try {
|
||||||
|
if (this.mOtrService == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final PublicKey publicKey = this.mOtrService.getPublicKey();
|
||||||
|
if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
|
||||||
|
return this.otrFingerprint;
|
||||||
|
} catch (final OtrCryptoException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.otrFingerprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getRosterVersion() {
|
public String getRosterVersion() {
|
||||||
if (this.rosterVersion == null) {
|
if (this.rosterVersion == null) {
|
||||||
return "";
|
return "";
|
||||||
|
@ -721,6 +756,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
|
|
||||||
private List<XmppUri.Fingerprint> getFingerprints() {
|
private List<XmppUri.Fingerprint> getFingerprints() {
|
||||||
ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
|
ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
|
||||||
|
final String otr = this.getOtrFingerprint();
|
||||||
|
if (otr != null) {
|
||||||
|
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR, otr));
|
||||||
|
}
|
||||||
if (axolotlService == null) {
|
if (axolotlService == null) {
|
||||||
return fingerprints;
|
return fingerprints;
|
||||||
}
|
}
|
||||||
|
|
|
@ -348,6 +348,47 @@ public class Contact implements ListItem, Blockable {
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ArrayList<String> getOtrFingerprints() {
|
||||||
|
synchronized (this.keys) {
|
||||||
|
final ArrayList<String> fingerprints = new ArrayList<String>();
|
||||||
|
try {
|
||||||
|
if (this.keys.has("otr_fingerprints")) {
|
||||||
|
final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
|
||||||
|
for (int i = 0; i < prints.length(); ++i) {
|
||||||
|
final String print = prints.isNull(i) ? null : prints.getString(i);
|
||||||
|
if (print != null && !print.isEmpty()) {
|
||||||
|
fingerprints.add(prints.getString(i).toLowerCase(Locale.US));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final JSONException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
return fingerprints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addOtrFingerprint(String print) {
|
||||||
|
synchronized (this.keys) {
|
||||||
|
if (getOtrFingerprints().contains(print)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSONArray fingerprints;
|
||||||
|
if (!this.keys.has("otr_fingerprints")) {
|
||||||
|
fingerprints = new JSONArray();
|
||||||
|
} else {
|
||||||
|
fingerprints = this.keys.getJSONArray("otr_fingerprints");
|
||||||
|
}
|
||||||
|
fingerprints.put(print);
|
||||||
|
this.keys.put("otr_fingerprints", fingerprints);
|
||||||
|
return true;
|
||||||
|
} catch (final JSONException ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public long getPgpKeyId() {
|
public long getPgpKeyId() {
|
||||||
synchronized (this.keys) {
|
synchronized (this.keys) {
|
||||||
if (this.keys.has("pgp_keyid")) {
|
if (this.keys.has("pgp_keyid")) {
|
||||||
|
|
|
@ -71,6 +71,7 @@ import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.security.interfaces.DSAPublicKey;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
@ -81,6 +82,7 @@ import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
|
@ -132,6 +134,12 @@ import me.saket.bettermovementmethod.BetterLinkMovementMethod;
|
||||||
|
|
||||||
import static eu.siacs.conversations.entities.Bookmark.printableValue;
|
import static eu.siacs.conversations.entities.Bookmark.printableValue;
|
||||||
|
|
||||||
|
import net.java.otr4j.OtrException;
|
||||||
|
import net.java.otr4j.crypto.OtrCryptoException;
|
||||||
|
import net.java.otr4j.session.SessionID;
|
||||||
|
import net.java.otr4j.session.SessionImpl;
|
||||||
|
import net.java.otr4j.session.SessionStatus;
|
||||||
|
|
||||||
|
|
||||||
public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
|
public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
|
||||||
public static final String TABLENAME = "conversations";
|
public static final String TABLENAME = "conversations";
|
||||||
|
@ -180,10 +188,15 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
private int mode;
|
private int mode;
|
||||||
private JSONObject attributes;
|
private JSONObject attributes;
|
||||||
private Jid nextCounterpart;
|
private Jid nextCounterpart;
|
||||||
|
private transient SessionImpl otrSession;
|
||||||
|
private transient String otrFingerprint = null;
|
||||||
|
private Smp mSmp = new Smp();
|
||||||
private transient MucOptions mucOptions = null;
|
private transient MucOptions mucOptions = null;
|
||||||
|
private byte[] symmetricKey;
|
||||||
private boolean messagesLeftOnServer = true;
|
private boolean messagesLeftOnServer = true;
|
||||||
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
|
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
|
||||||
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
|
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
|
||||||
|
private String mLastReceivedOtrMessageId = null;
|
||||||
private String mFirstMamReference = null;
|
private String mFirstMamReference = null;
|
||||||
protected Message replyTo = null;
|
protected Message replyTo = null;
|
||||||
protected int mCurrentTab = -1;
|
protected int mCurrentTab = -1;
|
||||||
|
@ -490,6 +503,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
|
||||||
|
synchronized (this.messages) {
|
||||||
|
for (Message message : this.messages) {
|
||||||
|
if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
|
||||||
|
&& (message.getEncryption() == encryptionType)) {
|
||||||
|
onMessageFound.onMessageFound(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void findUnsentTextMessages(OnMessageFound onMessageFound) {
|
public void findUnsentTextMessages(OnMessageFound onMessageFound) {
|
||||||
final ArrayList<Message> results = new ArrayList<>();
|
final ArrayList<Message> results = new ArrayList<>();
|
||||||
synchronized (this.messages) {
|
synchronized (this.messages) {
|
||||||
|
@ -662,6 +686,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
return getContact().getBlockedJid();
|
return getContact().getBlockedJid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getLastReceivedOtrMessageId() {
|
||||||
|
return this.mLastReceivedOtrMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastReceivedOtrMessageId(String id) {
|
||||||
|
this.mLastReceivedOtrMessageId = id;
|
||||||
|
}
|
||||||
|
|
||||||
public int countMessages() {
|
public int countMessages() {
|
||||||
synchronized (this.messages) {
|
synchronized (this.messages) {
|
||||||
return this.messages.size();
|
return this.messages.size();
|
||||||
|
@ -923,6 +955,124 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SessionImpl startOtrSession(String presence, boolean sendStart) {
|
||||||
|
if (this.otrSession != null) {
|
||||||
|
return this.otrSession;
|
||||||
|
} else {
|
||||||
|
final SessionID sessionId = new SessionID(this.getJid().asBareJid().toString(),
|
||||||
|
presence,
|
||||||
|
"xmpp");
|
||||||
|
this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
|
||||||
|
try {
|
||||||
|
if (sendStart) {
|
||||||
|
this.otrSession.startSession();
|
||||||
|
return this.otrSession;
|
||||||
|
}
|
||||||
|
return this.otrSession;
|
||||||
|
} catch (OtrException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public SessionImpl getOtrSession() {
|
||||||
|
return this.otrSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetOtrSession() {
|
||||||
|
this.otrFingerprint = null;
|
||||||
|
this.otrSession = null;
|
||||||
|
this.mSmp.hint = null;
|
||||||
|
this.mSmp.secret = null;
|
||||||
|
this.mSmp.status = Smp.STATUS_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Smp smp() {
|
||||||
|
return mSmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean startOtrIfNeeded() {
|
||||||
|
if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
|
||||||
|
try {
|
||||||
|
this.otrSession.startSession();
|
||||||
|
return true;
|
||||||
|
} catch (OtrException e) {
|
||||||
|
this.resetOtrSession();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean endOtrIfNeeded() {
|
||||||
|
if (this.otrSession != null) {
|
||||||
|
if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
|
||||||
|
try {
|
||||||
|
this.otrSession.endSession();
|
||||||
|
this.resetOtrSession();
|
||||||
|
return true;
|
||||||
|
} catch (OtrException e) {
|
||||||
|
this.resetOtrSession();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.resetOtrSession();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasValidOtrSession() {
|
||||||
|
return this.otrSession != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized String getOtrFingerprint() {
|
||||||
|
if (this.otrFingerprint == null) {
|
||||||
|
try {
|
||||||
|
if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
|
||||||
|
this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
|
||||||
|
} catch (final OtrCryptoException ignored) {
|
||||||
|
return null;
|
||||||
|
} catch (final UnsupportedOperationException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.otrFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verifyOtrFingerprint() {
|
||||||
|
final String fingerprint = getOtrFingerprint();
|
||||||
|
if (fingerprint != null) {
|
||||||
|
getContact().addOtrFingerprint(fingerprint);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOtrFingerprintVerified() {
|
||||||
|
return getContact().getOtrFingerprints().contains(getOtrFingerprint());
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Smp {
|
||||||
|
public static final int STATUS_NONE = 0;
|
||||||
|
public static final int STATUS_CONTACT_REQUESTED = 1;
|
||||||
|
public static final int STATUS_WE_REQUESTED = 2;
|
||||||
|
public static final int STATUS_FAILED = 3;
|
||||||
|
public static final int STATUS_VERIFIED = 4;
|
||||||
|
|
||||||
|
public String secret = null;
|
||||||
|
public String hint = null;
|
||||||
|
public int status = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* short for is Private and Non-anonymous
|
* short for is Private and Non-anonymous
|
||||||
*/
|
*/
|
||||||
|
@ -964,7 +1114,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getNextEncryption() {
|
public int getNextEncryption() {
|
||||||
if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
|
if (!Config.supportOmemo() && !Config.supportOpenPgp() && !Config.supportOtr()) {
|
||||||
return Message.ENCRYPTION_NONE;
|
return Message.ENCRYPTION_NONE;
|
||||||
}
|
}
|
||||||
if (OmemoSetting.isAlways()) {
|
if (OmemoSetting.isAlways()) {
|
||||||
|
@ -993,6 +1143,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
return nextMessage == null ? "" : nextMessage;
|
return nextMessage == null ? "" : nextMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean smpRequested() {
|
||||||
|
return smp().status == Smp.STATUS_CONTACT_REQUESTED;
|
||||||
|
}
|
||||||
|
|
||||||
public @Nullable
|
public @Nullable
|
||||||
Draft getDraft() {
|
Draft getDraft() {
|
||||||
long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
|
long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
|
||||||
|
@ -1015,6 +1169,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSymmetricKey(byte[] key) {
|
||||||
|
this.symmetricKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSymmetricKey() {
|
||||||
|
return this.symmetricKey;
|
||||||
|
}
|
||||||
|
|
||||||
public Bookmark getBookmark() {
|
public Bookmark getBookmark() {
|
||||||
return this.account.getBookmark(this.contactJid);
|
return this.account.getBookmark(this.contactJid);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import java.util.List;
|
||||||
public interface Transferable {
|
public interface Transferable {
|
||||||
|
|
||||||
List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
|
List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
|
||||||
List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg");
|
List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg", "otr");
|
||||||
|
|
||||||
int STATUS_UNKNOWN = 0x200;
|
int STATUS_UNKNOWN = 0x200;
|
||||||
int STATUS_CHECKING = 0x201;
|
int STATUS_CHECKING = 0x201;
|
||||||
|
|
|
@ -57,6 +57,9 @@ public abstract class AbstractGenerator {
|
||||||
private final String[] PRIVACY_SENSITIVE = {
|
private final String[] PRIVACY_SENSITIVE = {
|
||||||
"urn:xmpp:time" //XEP-0202: Entity Time leaks time zone
|
"urn:xmpp:time" //XEP-0202: Entity Time leaks time zone
|
||||||
};
|
};
|
||||||
|
private final String[] OTR = {
|
||||||
|
"urn:xmpp:otr:0"
|
||||||
|
};
|
||||||
private final String[] VOIP_NAMESPACES = {
|
private final String[] VOIP_NAMESPACES = {
|
||||||
Namespace.JINGLE_TRANSPORT_ICE_UDP,
|
Namespace.JINGLE_TRANSPORT_ICE_UDP,
|
||||||
Namespace.JINGLE_FEATURE_AUDIO,
|
Namespace.JINGLE_FEATURE_AUDIO,
|
||||||
|
@ -125,6 +128,9 @@ public abstract class AbstractGenerator {
|
||||||
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
|
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
|
||||||
features.addAll(Arrays.asList(VOIP_NAMESPACES));
|
features.addAll(Arrays.asList(VOIP_NAMESPACES));
|
||||||
}
|
}
|
||||||
|
if (Config.supportOtr()) {
|
||||||
|
features.addAll(Arrays.asList(OTR));
|
||||||
|
}
|
||||||
if (mXmppConnectionService.broadcastLastActivity()) {
|
if (mXmppConnectionService.broadcastLastActivity()) {
|
||||||
features.add(Namespace.IDLE);
|
features.add(Namespace.IDLE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package eu.siacs.conversations.generator;
|
package eu.siacs.conversations.generator;
|
||||||
|
|
||||||
|
import net.java.otr4j.OtrException;
|
||||||
|
import net.java.otr4j.session.Session;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -24,6 +27,7 @@ import eu.siacs.conversations.xmpp.jingle.Media;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
|
||||||
public class MessageGenerator extends AbstractGenerator {
|
public class MessageGenerator extends AbstractGenerator {
|
||||||
|
public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that";
|
||||||
private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo";
|
private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo";
|
||||||
private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that.";
|
private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that.";
|
||||||
|
|
||||||
|
@ -102,6 +106,36 @@ public class MessageGenerator extends AbstractGenerator {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void addMessageHints(MessagePacket packet) {
|
||||||
|
packet.addChild("private", "urn:xmpp:carbons:2");
|
||||||
|
packet.addChild("no-copy", "urn:xmpp:hints");
|
||||||
|
packet.addChild("no-permanent-store", "urn:xmpp:hints");
|
||||||
|
packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store*
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessagePacket generateOtrChat(Message message) {
|
||||||
|
Conversation conversation = (Conversation) message.getConversation();
|
||||||
|
Session otrSession = conversation.getOtrSession();
|
||||||
|
if (otrSession == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MessagePacket packet = preparePacket(message);
|
||||||
|
addMessageHints(packet);
|
||||||
|
try {
|
||||||
|
String content;
|
||||||
|
if (message.hasFileOnRemoteHost()) {
|
||||||
|
content = message.getFileParams().url.toString();
|
||||||
|
} else {
|
||||||
|
content = message.getBody();
|
||||||
|
}
|
||||||
|
packet.setBody(otrSession.transformSending(content)[0]);
|
||||||
|
packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0");
|
||||||
|
return packet;
|
||||||
|
} catch (OtrException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public MessagePacket generateChat(Message message) {
|
public MessagePacket generateChat(Message message) {
|
||||||
MessagePacket packet = preparePacket(message);
|
MessagePacket packet = preparePacket(message);
|
||||||
String content;
|
String content;
|
||||||
|
@ -233,6 +267,19 @@ public class MessageGenerator extends AbstractGenerator {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MessagePacket generateOtrError(Jid to, String id, String errorText) {
|
||||||
|
MessagePacket packet = new MessagePacket();
|
||||||
|
packet.setType(MessagePacket.TYPE_ERROR);
|
||||||
|
packet.setAttribute("id", id);
|
||||||
|
packet.setTo(to);
|
||||||
|
Element error = packet.addChild("error");
|
||||||
|
error.setAttribute("code", "406");
|
||||||
|
error.setAttribute("type", "modify");
|
||||||
|
error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas");
|
||||||
|
error.addChild("text").setContent("?OTR Error:" + errorText);
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
|
public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) {
|
||||||
final MessagePacket packet = new MessagePacket();
|
final MessagePacket packet = new MessagePacket();
|
||||||
packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
|
packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
package eu.siacs.conversations.parser;
|
package eu.siacs.conversations.parser;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.Html;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import net.java.otr4j.session.Session;
|
||||||
|
import net.java.otr4j.session.SessionStatus;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -16,6 +21,7 @@ import java.util.UUID;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
|
import eu.siacs.conversations.crypto.OtrService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
|
import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
|
||||||
import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
|
import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException;
|
||||||
|
@ -28,9 +34,11 @@ import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Conversational;
|
import eu.siacs.conversations.entities.Conversational;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.MucOptions;
|
import eu.siacs.conversations.entities.MucOptions;
|
||||||
|
import eu.siacs.conversations.entities.Presence;
|
||||||
import eu.siacs.conversations.entities.ReadByMarker;
|
import eu.siacs.conversations.entities.ReadByMarker;
|
||||||
import eu.siacs.conversations.entities.ReceiptRequest;
|
import eu.siacs.conversations.entities.ReceiptRequest;
|
||||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||||
|
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||||
import eu.siacs.conversations.services.MessageArchiveService;
|
import eu.siacs.conversations.services.MessageArchiveService;
|
||||||
import eu.siacs.conversations.services.QuickConversationsService;
|
import eu.siacs.conversations.services.QuickConversationsService;
|
||||||
|
@ -49,6 +57,7 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
|
||||||
public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
|
public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
|
||||||
|
private static final List<String> CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian");
|
||||||
|
|
||||||
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
|
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH);
|
||||||
|
|
||||||
|
@ -95,6 +104,30 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
return result != null ? result : fallback;
|
return result != null ? result : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean clientMightSendHtml(Account account, Jid from) {
|
||||||
|
String resource = from.getResource();
|
||||||
|
if (resource == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Presence presence = account.getRoster().getContact(from).getPresences().getPresencesMap().get(resource);
|
||||||
|
ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult();
|
||||||
|
if (disco == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return hasIdentityKnowForSendingHtml(disco.getIdentities());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasIdentityKnowForSendingHtml(List<ServiceDiscoveryResult.Identity> identities) {
|
||||||
|
for (ServiceDiscoveryResult.Identity identity : identities) {
|
||||||
|
if (identity.getName() != null) {
|
||||||
|
if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
|
private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) {
|
||||||
ChatState state = ChatState.parse(packet);
|
ChatState state = ChatState.parse(packet);
|
||||||
if (state != null && c != null) {
|
if (state != null && c != null) {
|
||||||
|
@ -126,6 +159,66 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) {
|
||||||
|
String presence;
|
||||||
|
if (from.isBareJid()) {
|
||||||
|
presence = "";
|
||||||
|
} else {
|
||||||
|
presence = from.getResource();
|
||||||
|
}
|
||||||
|
if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) {
|
||||||
|
conversation.endOtrIfNeeded();
|
||||||
|
}
|
||||||
|
if (!conversation.hasValidOtrSession()) {
|
||||||
|
conversation.startOtrSession(presence, false);
|
||||||
|
} else {
|
||||||
|
String foreignPresence = conversation.getOtrSession().getSessionID().getUserID();
|
||||||
|
if (!foreignPresence.equals(presence)) {
|
||||||
|
conversation.endOtrIfNeeded();
|
||||||
|
conversation.startOtrSession(presence, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
conversation.setLastReceivedOtrMessageId(id);
|
||||||
|
Session otrSession = conversation.getOtrSession();
|
||||||
|
body = otrSession.transformReceiving(body);
|
||||||
|
SessionStatus status = otrSession.getSessionStatus();
|
||||||
|
if (body == null && status == SessionStatus.ENCRYPTED) {
|
||||||
|
mXmppConnectionService.onOtrSessionEstablished(conversation);
|
||||||
|
return null;
|
||||||
|
} else if (body == null && status == SessionStatus.FINISHED) {
|
||||||
|
conversation.resetOtrSession();
|
||||||
|
mXmppConnectionService.updateConversationUi();
|
||||||
|
return null;
|
||||||
|
} else if (body == null || (body.isEmpty())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (body.startsWith(CryptoHelper.FILETRANSFER)) {
|
||||||
|
String key = body.substring(CryptoHelper.FILETRANSFER.length());
|
||||||
|
conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (clientMightSendHtml(conversation.getAccount(), from)) {
|
||||||
|
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OTR message from bad behaving client. escaping HTML…");
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
body = Html.fromHtml(body, Html.FROM_HTML_MODE_LEGACY).toString();
|
||||||
|
} else {
|
||||||
|
body = Html.fromHtml(body).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final OtrService otrService = conversation.getAccount().getOtrService();
|
||||||
|
Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED);
|
||||||
|
finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey()));
|
||||||
|
conversation.setLastReceivedOtrMessageId(null);
|
||||||
|
|
||||||
|
return finishedMessage;
|
||||||
|
} catch (Exception e) {
|
||||||
|
conversation.resetOtrSession();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) {
|
private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) {
|
||||||
final AxolotlService service = conversation.getAccount().getAxolotlService();
|
final AxolotlService service = conversation.getAccount().getAxolotlService();
|
||||||
final XmppAxolotlMessage xmppAxolotlMessage;
|
final XmppAxolotlMessage xmppAxolotlMessage;
|
||||||
|
@ -327,6 +420,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
final Jid from = packet.getFrom();
|
final Jid from = packet.getFrom();
|
||||||
final String id = packet.getId();
|
final String id = packet.getId();
|
||||||
if (from != null && id != null) {
|
if (from != null && id != null) {
|
||||||
|
final Message message = mXmppConnectionService.markMessage(account,
|
||||||
|
from.asBareJid(),
|
||||||
|
packet.getId(),
|
||||||
|
Message.STATUS_SEND_FAILED,
|
||||||
|
extractErrorMessage(packet));
|
||||||
if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
|
if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
|
||||||
final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
|
final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
|
||||||
mXmppConnectionService.getJingleConnectionManager()
|
mXmppConnectionService.getJingleConnectionManager()
|
||||||
|
@ -335,8 +433,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
|
if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
|
||||||
final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
|
final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
|
||||||
final String message = extractErrorMessage(packet);
|
final String errorMessage = extractErrorMessage(packet);
|
||||||
mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message);
|
mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, errorMessage);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
mXmppConnectionService.markMessage(account,
|
mXmppConnectionService.markMessage(account,
|
||||||
|
@ -355,6 +453,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||||
|
Conversation conversation = (Conversation) message.getConversation();
|
||||||
|
conversation.endOtrIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -368,6 +473,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
final MessagePacket packet;
|
final MessagePacket packet;
|
||||||
Long timestamp = null;
|
Long timestamp = null;
|
||||||
|
final boolean isForwarded;
|
||||||
boolean isCarbon = false;
|
boolean isCarbon = false;
|
||||||
String serverMsgId = null;
|
String serverMsgId = null;
|
||||||
final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
|
final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace);
|
||||||
|
@ -385,7 +491,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
timestamp = f.second;
|
timestamp = f.second;
|
||||||
packet = f.first;
|
packet = f.first;
|
||||||
|
isForwarded = true;
|
||||||
serverMsgId = result.getAttribute("id");
|
serverMsgId = result.getAttribute("id");
|
||||||
|
|
||||||
query.incrementMessageCount();
|
query.incrementMessageCount();
|
||||||
if (handleErrorMessage(account, packet)) {
|
if (handleErrorMessage(account, packet)) {
|
||||||
return;
|
return;
|
||||||
|
@ -403,8 +511,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
timestamp = f != null ? f.second : null;
|
timestamp = f != null ? f.second : null;
|
||||||
isCarbon = f != null;
|
isCarbon = f != null;
|
||||||
|
isForwarded = isCarbon;
|
||||||
} else {
|
} else {
|
||||||
packet = original;
|
packet = original;
|
||||||
|
isForwarded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timestamp == null) {
|
if (timestamp == null) {
|
||||||
|
@ -449,6 +559,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
|
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
boolean isProperlyAddressed = (to != null) && (!to.isBareJid() || account.countPresences() == 0);
|
||||||
boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
|
boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
|
||||||
boolean selfAddressed;
|
boolean selfAddressed;
|
||||||
if (packet.fromAccount(account)) {
|
if (packet.fromAccount(account)) {
|
||||||
|
@ -547,7 +658,20 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final Message message;
|
final Message message;
|
||||||
if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
if (body != null && body.content.startsWith("?OTR") && Config.supportOtr()) {
|
||||||
|
if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) {
|
||||||
|
message = parseOtrChat(body.content, from, remoteMsgId, conversation);
|
||||||
|
if (message == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed));
|
||||||
|
message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
|
||||||
|
if (body.count > 1) {
|
||||||
|
message.setBodyLanguage(body.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
||||||
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
||||||
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
|
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
|
||||||
Jid origin;
|
Jid origin;
|
||||||
|
@ -796,6 +920,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
||||||
processMessageReceipts(account, packet, remoteMsgId, query);
|
processMessageReceipts(account, packet, remoteMsgId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.getStatus() == Message.STATUS_RECEIVED
|
||||||
|
&& conversation.getOtrSession() != null
|
||||||
|
&& !conversation.getOtrSession().getSessionID().getUserID()
|
||||||
|
.equals(message.getCounterpart().getResource())) {
|
||||||
|
conversation.endOtrIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
mXmppConnectionService.databaseBackend.createMessage(message);
|
mXmppConnectionService.databaseBackend.createMessage(message);
|
||||||
final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
|
final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
|
||||||
if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
|
if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {
|
||||||
|
|
|
@ -61,6 +61,11 @@ import com.google.common.base.Objects;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
|
import net.java.otr4j.session.Session;
|
||||||
|
import net.java.otr4j.session.SessionID;
|
||||||
|
import net.java.otr4j.session.SessionImpl;
|
||||||
|
import net.java.otr4j.session.SessionStatus;
|
||||||
|
|
||||||
import org.conscrypt.Conscrypt;
|
import org.conscrypt.Conscrypt;
|
||||||
import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
|
import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
|
||||||
import org.openintents.openpgp.IOpenPgpService2;
|
import org.openintents.openpgp.IOpenPgpService2;
|
||||||
|
@ -171,6 +176,7 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
||||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||||
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
import eu.siacs.conversations.xmpp.forms.Data;
|
import eu.siacs.conversations.xmpp.forms.Data;
|
||||||
|
import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
|
||||||
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
|
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
|
||||||
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
|
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
|
||||||
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
||||||
|
@ -255,9 +261,18 @@ public class XmppConnectionService extends Service {
|
||||||
Conversation conversation = find(getConversations(), contact);
|
Conversation conversation = find(getConversations(), contact);
|
||||||
if (conversation != null) {
|
if (conversation != null) {
|
||||||
if (online) {
|
if (online) {
|
||||||
|
conversation.endOtrIfNeeded();
|
||||||
if (contact.getPresences().size() == 1) {
|
if (contact.getPresences().size() == 1) {
|
||||||
sendUnsentMessages(conversation);
|
sendUnsentMessages(conversation);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
//check if the resource we are haveing a conversation with is still online
|
||||||
|
if (conversation.hasValidOtrSession()) {
|
||||||
|
String otrResource = conversation.getOtrSession().getSessionID().getUserID();
|
||||||
|
if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) {
|
||||||
|
conversation.endOtrIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -447,6 +462,9 @@ public class XmppConnectionService extends Service {
|
||||||
if (conversation.getAccount() == account
|
if (conversation.getAccount() == account
|
||||||
&& !pendingJoin
|
&& !pendingJoin
|
||||||
&& !inProgressJoin) {
|
&& !inProgressJoin) {
|
||||||
|
if (!conversation.startOtrIfNeeded()) {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": couldn't start OTR with " + conversation.getContact().getJid() + " when needed");
|
||||||
|
}
|
||||||
sendUnsentMessages(conversation);
|
sendUnsentMessages(conversation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1704,6 +1722,12 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
|
||||||
|
conversation.endOtrIfNeeded();
|
||||||
|
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
|
||||||
|
message1 -> markMessage(message1, Message.STATUS_SEND_FAILED));
|
||||||
|
}
|
||||||
|
|
||||||
final boolean inProgressJoin = isJoinInProgress(conversation);
|
final boolean inProgressJoin = isJoinInProgress(conversation);
|
||||||
|
|
||||||
|
|
||||||
|
@ -1736,6 +1760,30 @@ public class XmppConnectionService extends Service {
|
||||||
packet = mMessageGenerator.generatePgpChat(message);
|
packet = mMessageGenerator.generatePgpChat(message);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Message.ENCRYPTION_OTR:
|
||||||
|
SessionImpl otrSession = conversation.getOtrSession();
|
||||||
|
if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
|
||||||
|
try {
|
||||||
|
message.setCounterpart(OtrJidHelper.fromSessionID(otrSession.getSessionID()));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (message.needsUploading()) {
|
||||||
|
mJingleConnectionManager.startJingleFileTransfer(message);
|
||||||
|
} else {
|
||||||
|
packet = mMessageGenerator.generateOtrChat(message);
|
||||||
|
}
|
||||||
|
} else if (otrSession == null) {
|
||||||
|
if (message.fixCounterpart()) {
|
||||||
|
conversation.startOtrSession(message.getCounterpart().getResource(), true);
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fix counterpart for OTR message to contact " + message.getCounterpart());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " OTR session with " + message.getContact() + " is in wrong state: " + otrSession.getSessionStatus().toString());
|
||||||
|
}
|
||||||
|
break;
|
||||||
case Message.ENCRYPTION_AXOLOTL:
|
case Message.ENCRYPTION_AXOLOTL:
|
||||||
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
|
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
|
||||||
if (message.needsUploading()) {
|
if (message.needsUploading()) {
|
||||||
|
@ -1789,6 +1837,12 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Message.ENCRYPTION_OTR:
|
||||||
|
if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": create otr session without starting for " + message.getContact().getJid());
|
||||||
|
conversation.startOtrSession(message.getCounterpart().getResource(), false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case Message.ENCRYPTION_AXOLOTL:
|
case Message.ENCRYPTION_AXOLOTL:
|
||||||
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
|
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
|
||||||
break;
|
break;
|
||||||
|
@ -3922,6 +3976,12 @@ public class XmppConnectionService extends Service {
|
||||||
if (conversation.getAccount() == account) {
|
if (conversation.getAccount() == account) {
|
||||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||||
leaveMuc(conversation, true);
|
leaveMuc(conversation, true);
|
||||||
|
} else {
|
||||||
|
if (conversation.endOtrIfNeeded()) {
|
||||||
|
Log.d(Config.LOGTAG, account.getJid().asBareJid()
|
||||||
|
+ ": ended otr session with "
|
||||||
|
+ conversation.getJid());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3978,6 +4038,39 @@ public class XmppConnectionService extends Service {
|
||||||
pushContactToServer(contact, preAuth);
|
pushContactToServer(contact, preAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onOtrSessionEstablished(Conversation conversation) {
|
||||||
|
final Account account = conversation.getAccount();
|
||||||
|
final Session otrSession = conversation.getOtrSession();
|
||||||
|
Log.d(Config.LOGTAG,
|
||||||
|
account.getJid().asBareJid() + " otr session established with "
|
||||||
|
+ conversation.getJid() + "/"
|
||||||
|
+ otrSession.getSessionID().getUserID());
|
||||||
|
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessageFound(Message message) {
|
||||||
|
SessionID id = otrSession.getSessionID();
|
||||||
|
try {
|
||||||
|
message.setCounterpart(Jid.of(id.getAccountID() + "/" + id.getUserID()));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.needsUploading()) {
|
||||||
|
mJingleConnectionManager.startJingleFileTransfer(message);
|
||||||
|
} else {
|
||||||
|
MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
|
||||||
|
if (outPacket != null) {
|
||||||
|
mMessageGenerator.addDelay(outPacket, message.getTimeSent());
|
||||||
|
message.setStatus(Message.STATUS_SEND);
|
||||||
|
databaseBackend.updateMessage(message, false);
|
||||||
|
sendMessagePacket(account, outPacket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateConversationUi();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void pushContactToServer(final Contact contact) {
|
public void pushContactToServer(final Contact contact) {
|
||||||
pushContactToServer(contact, null);
|
pushContactToServer(contact, null);
|
||||||
}
|
}
|
||||||
|
@ -4503,6 +4596,7 @@ public class XmppConnectionService extends Service {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
final Message message = conversation.findSentMessageWithUuid(uuid);
|
final Message message = conversation.findSentMessageWithUuid(uuid);
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
if (message.getServerMsgId() == null) {
|
if (message.getServerMsgId() == null) {
|
||||||
message.setServerMsgId(serverMessageId);
|
message.setServerMsgId(serverMessageId);
|
||||||
|
@ -4805,6 +4899,11 @@ public class XmppConnectionService extends Service {
|
||||||
setMemorizingTrustManager(tm);
|
setMemorizingTrustManager(tm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void syncRosterToDisk(final Account account) {
|
||||||
|
Runnable runnable = () -> databaseBackend.writeRoster(account.getRoster());
|
||||||
|
mDatabaseWriterExecutor.execute(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
public LruCache<String, Bitmap> getBitmapCache() {
|
public LruCache<String, Bitmap> getBitmapCache() {
|
||||||
return this.mBitmapCache;
|
return this.mBitmapCache;
|
||||||
}
|
}
|
||||||
|
@ -5272,10 +5371,14 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
|
public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
|
||||||
|
boolean needsRosterWrite = false;
|
||||||
boolean performedVerification = false;
|
boolean performedVerification = false;
|
||||||
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
|
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
|
||||||
for (XmppUri.Fingerprint fp : fingerprints) {
|
for (XmppUri.Fingerprint fp : fingerprints) {
|
||||||
if (fp.type == XmppUri.FingerprintType.OMEMO) {
|
if (fp.type == XmppUri.FingerprintType.OTR) {
|
||||||
|
performedVerification |= contact.addOtrFingerprint(fp.fingerprint);
|
||||||
|
needsRosterWrite |= performedVerification;
|
||||||
|
} else if (fp.type == XmppUri.FingerprintType.OMEMO) {
|
||||||
String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
|
String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
|
||||||
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
|
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
|
||||||
if (fingerprintStatus != null) {
|
if (fingerprintStatus != null) {
|
||||||
|
@ -5288,6 +5391,11 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needsRosterWrite) {
|
||||||
|
syncRosterToDisk(contact.getAccount());
|
||||||
|
}
|
||||||
|
|
||||||
return performedVerification;
|
return performedVerification;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,8 @@ import androidx.viewpager.widget.PagerAdapter;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
|
import net.java.otr4j.session.SessionStatus;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -223,6 +225,14 @@ public class ConversationFragment extends XmppFragment
|
||||||
private ConversationsActivity activity;
|
private ConversationsActivity activity;
|
||||||
private Vibrator vibrator;
|
private Vibrator vibrator;
|
||||||
private boolean reInitRequiredOnStart = true;
|
private boolean reInitRequiredOnStart = true;
|
||||||
|
|
||||||
|
protected OnClickListener clickToVerify = new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
activity.verifyOtrSessionDialog(conversation, v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private int primaryColor = -1;
|
private int primaryColor = -1;
|
||||||
|
|
||||||
|
@ -534,6 +544,20 @@ public class ConversationFragment extends XmppFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private OnClickListener mAnswerSmpClickListener = new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
Intent intent = new Intent(activity, VerifyOTRActivity.class);
|
||||||
|
intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
|
||||||
|
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
|
||||||
|
intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
|
||||||
|
intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION);
|
||||||
|
startActivity(intent);
|
||||||
|
activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
protected OnClickListener clickToDecryptListener =
|
protected OnClickListener clickToDecryptListener =
|
||||||
new OnClickListener() {
|
new OnClickListener() {
|
||||||
|
|
||||||
|
@ -1064,6 +1088,8 @@ public class ConversationFragment extends XmppFragment
|
||||||
message.setUuid(UUID.randomUUID().toString());
|
message.setUuid(UUID.randomUUID().toString());
|
||||||
}
|
}
|
||||||
switch (conversation.getNextEncryption()) {
|
switch (conversation.getNextEncryption()) {
|
||||||
|
case Message.ENCRYPTION_OTR:
|
||||||
|
sendOtrMessage(message);
|
||||||
case Message.ENCRYPTION_PGP:
|
case Message.ENCRYPTION_PGP:
|
||||||
sendPgpMessage(message);
|
sendPgpMessage(message);
|
||||||
break;
|
break;
|
||||||
|
@ -3550,6 +3576,14 @@ public class ConversationFragment extends XmppFragment
|
||||||
}
|
}
|
||||||
} else if (account.hasPendingPgpIntent(conversation)) {
|
} else if (account.hasPendingPgpIntent(conversation)) {
|
||||||
showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
|
showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener);
|
||||||
|
} else if (mode == Conversation.MODE_SINGLE
|
||||||
|
&& conversation.smpRequested()) {
|
||||||
|
showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener);
|
||||||
|
} else if (mode == Conversation.MODE_SINGLE
|
||||||
|
&& conversation.hasValidOtrSession()
|
||||||
|
&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED)
|
||||||
|
&& (!conversation.isOtrFingerprintVerified())) {
|
||||||
|
showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify);
|
||||||
} else if (connection != null
|
} else if (connection != null
|
||||||
&& connection.getFeatures().blocking()
|
&& connection.getFeatures().blocking()
|
||||||
&& conversation.countMessages() != 0
|
&& conversation.countMessages() != 0
|
||||||
|
@ -3650,6 +3684,11 @@ public class ConversationFragment extends XmppFragment
|
||||||
new Handler()
|
new Handler()
|
||||||
.post(
|
.post(
|
||||||
() -> {
|
() -> {
|
||||||
|
if (conversation.isInHistoryPart()) {
|
||||||
|
conversation.jumpToLatest();
|
||||||
|
refresh(false);
|
||||||
|
}
|
||||||
|
|
||||||
int size = messageList.size();
|
int size = messageList.size();
|
||||||
this.binding.messagesView.setSelection(size - 1);
|
this.binding.messagesView.setSelection(size - 1);
|
||||||
});
|
});
|
||||||
|
@ -3937,6 +3976,17 @@ public class ConversationFragment extends XmppFragment
|
||||||
messageSent();
|
messageSent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void sendOtrMessage(final Message message) {
|
||||||
|
final ConversationsActivity activity = (ConversationsActivity) getActivity();
|
||||||
|
final XmppConnectionService xmppService = activity.xmppConnectionService;
|
||||||
|
activity.selectPresence(conversation,
|
||||||
|
() -> {
|
||||||
|
message.setCounterpart(conversation.getNextCounterpart());
|
||||||
|
xmppService.sendMessage(message);
|
||||||
|
messageSent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected void sendPgpMessage(final Message message) {
|
protected void sendPgpMessage(final Message message) {
|
||||||
final XmppConnectionService xmppService = activity.xmppConnectionService;
|
final XmppConnectionService xmppService = activity.xmppConnectionService;
|
||||||
final Contact contact = message.getConversation().getContact();
|
final Contact contact = message.getConversation().getContact();
|
||||||
|
|
|
@ -63,9 +63,12 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.widget.PopupMenu;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.databinding.DataBindingUtil;
|
import androidx.databinding.DataBindingUtil;
|
||||||
|
|
||||||
|
import net.java.otr4j.session.SessionStatus;
|
||||||
|
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
import com.google.android.material.navigation.NavigationBarView;
|
import com.google.android.material.navigation.NavigationBarView;
|
||||||
|
|
||||||
|
@ -105,6 +108,7 @@ import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
||||||
import io.michaelrocks.libphonenumber.android.NumberParseException;
|
import io.michaelrocks.libphonenumber.android.NumberParseException;
|
||||||
|
import me.drakeet.support.toast.ToastCompat;
|
||||||
|
|
||||||
public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged {
|
public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged {
|
||||||
|
|
||||||
|
@ -812,6 +816,33 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void verifyOtrSessionDialog(final Conversation conversation, View view) {
|
||||||
|
if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
|
||||||
|
ToastCompat.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (view == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PopupMenu popup = new PopupMenu(this, view);
|
||||||
|
popup.inflate(R.menu.verification_choices);
|
||||||
|
popup.setOnMenuItemClickListener(menuItem -> {
|
||||||
|
Intent intent = new Intent(ConversationsActivity.this, VerifyOTRActivity.class);
|
||||||
|
intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT);
|
||||||
|
intent.putExtra("contact", conversation.getContact().getJid().asBareJid().toString());
|
||||||
|
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
|
||||||
|
switch (menuItem.getItemId()) {
|
||||||
|
case R.id.ask_question:
|
||||||
|
intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startActivity(intent);
|
||||||
|
overridePendingTransition(R.animator.fade_in, R.animator.fade_out);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
popup.show();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConversationArchived(Conversation conversation) {
|
public void onConversationArchived(Conversation conversation) {
|
||||||
if (performRedirectIfNecessary(conversation, false)) {
|
if (performRedirectIfNecessary(conversation, false)) {
|
||||||
|
|
|
@ -265,7 +265,6 @@ public class SendLogActivity extends ActionBarActivity {
|
||||||
log.insert(0, mAdditonalInfo);
|
log.insert(0, mAdditonalInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
android.util.Log.e("35fd", log.toString());
|
|
||||||
writer.write(log.toString());
|
writer.write(log.toString());
|
||||||
}
|
}
|
||||||
catch (IOException e){
|
catch (IOException e){
|
||||||
|
|
450
src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
Normal file
450
src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java
Normal file
|
@ -0,0 +1,450 @@
|
||||||
|
package eu.siacs.conversations.ui;
|
||||||
|
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import net.java.otr4j.OtrException;
|
||||||
|
import net.java.otr4j.session.Session;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.R;
|
||||||
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
import eu.siacs.conversations.entities.Contact;
|
||||||
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
import eu.siacs.conversations.utils.XmppUri;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import me.drakeet.support.toast.ToastCompat;
|
||||||
|
|
||||||
|
public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate {
|
||||||
|
|
||||||
|
public static final String ACTION_VERIFY_CONTACT = "verify_contact";
|
||||||
|
public static final int MODE_SCAN_FINGERPRINT = -0x0502;
|
||||||
|
public static final int MODE_ASK_QUESTION = 0x0503;
|
||||||
|
public static final int MODE_ANSWER_QUESTION = 0x0504;
|
||||||
|
public static final int MODE_MANUAL_VERIFICATION = 0x0505;
|
||||||
|
|
||||||
|
private LinearLayout mManualVerificationArea;
|
||||||
|
private LinearLayout mSmpVerificationArea;
|
||||||
|
private TextView mRemoteFingerprint;
|
||||||
|
private TextView mYourFingerprint;
|
||||||
|
private TextView mVerificationExplain;
|
||||||
|
private TextView mStatusMessage;
|
||||||
|
private TextView mSharedSecretHint;
|
||||||
|
private EditText mSharedSecretHintEditable;
|
||||||
|
private EditText mSharedSecretSecret;
|
||||||
|
private Button mLeftButton;
|
||||||
|
private Button mRightButton;
|
||||||
|
private Account mAccount;
|
||||||
|
private Conversation mConversation;
|
||||||
|
private int mode = MODE_MANUAL_VERIFICATION;
|
||||||
|
private XmppUri mPendingUri = null;
|
||||||
|
|
||||||
|
private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialogInterface, int click) {
|
||||||
|
mConversation.verifyOtrFingerprint();
|
||||||
|
xmppConnectionService.syncRosterToDisk(mConversation.getAccount());
|
||||||
|
ToastCompat.makeText(VerifyOTRActivity.this, R.string.verified, Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(final View view) {
|
||||||
|
if (isAccountOnline()) {
|
||||||
|
final String question = mSharedSecretHintEditable.getText().toString();
|
||||||
|
final String secret = mSharedSecretSecret.getText().toString();
|
||||||
|
if (question.trim().isEmpty()) {
|
||||||
|
mSharedSecretHintEditable.requestFocus();
|
||||||
|
mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty));
|
||||||
|
} else if (secret.trim().isEmpty()) {
|
||||||
|
mSharedSecretSecret.requestFocus();
|
||||||
|
mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty));
|
||||||
|
} else {
|
||||||
|
mSharedSecretSecret.setError(null);
|
||||||
|
mSharedSecretHintEditable.setError(null);
|
||||||
|
initSmp(question, secret);
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
if (isAccountOnline()) {
|
||||||
|
abortSmp();
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
if (isAccountOnline()) {
|
||||||
|
final String question = mSharedSecretHintEditable.getText().toString();
|
||||||
|
final String secret = mSharedSecretSecret.getText().toString();
|
||||||
|
respondSmp(question, secret);
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
mConversation.smp().status = Conversation.Smp.STATUS_NONE;
|
||||||
|
mConversation.smp().hint = null;
|
||||||
|
mConversation.smp().secret = null;
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private View.OnClickListener mFinishListener = new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
mConversation.smp().status = Conversation.Smp.STATUS_NONE;
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected boolean initSmp(final String question, final String secret) {
|
||||||
|
final Session session = mConversation.getOtrSession();
|
||||||
|
if (session != null) {
|
||||||
|
try {
|
||||||
|
session.initSmp(question, secret);
|
||||||
|
mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED;
|
||||||
|
mConversation.smp().secret = secret;
|
||||||
|
mConversation.smp().hint = question;
|
||||||
|
return true;
|
||||||
|
} catch (OtrException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean abortSmp() {
|
||||||
|
final Session session = mConversation.getOtrSession();
|
||||||
|
if (session != null) {
|
||||||
|
try {
|
||||||
|
session.abortSmp();
|
||||||
|
mConversation.smp().status = Conversation.Smp.STATUS_NONE;
|
||||||
|
mConversation.smp().hint = null;
|
||||||
|
mConversation.smp().secret = null;
|
||||||
|
return true;
|
||||||
|
} catch (OtrException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean respondSmp(final String question, final String secret) {
|
||||||
|
final Session session = mConversation.getOtrSession();
|
||||||
|
if (session != null) {
|
||||||
|
try {
|
||||||
|
session.respondSmp(question, secret);
|
||||||
|
return true;
|
||||||
|
} catch (OtrException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean verifyWithUri(XmppUri uri) {
|
||||||
|
Contact contact = mConversation.getContact();
|
||||||
|
if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) {
|
||||||
|
xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints());
|
||||||
|
ToastCompat.makeText(this, R.string.verified, Toast.LENGTH_SHORT).show();
|
||||||
|
updateView();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
ToastCompat.makeText(this, R.string.could_not_verify_fingerprint, Toast.LENGTH_SHORT).show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isAccountOnline() {
|
||||||
|
if (this.mAccount.getStatus() != Account.State.ONLINE) {
|
||||||
|
ToastCompat.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean handleIntent(Intent intent) {
|
||||||
|
if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) {
|
||||||
|
this.mAccount = extractAccount(intent);
|
||||||
|
if (this.mAccount == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.mConversation = this.xmppConnectionService.find(this.mAccount, Jid.of(intent.getExtras().getString("contact")), null);
|
||||||
|
if (this.mConversation == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (final IllegalArgumentException ignored) {
|
||||||
|
ignored.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION);
|
||||||
|
// todo scan OTR fingerprint
|
||||||
|
if (this.mode == MODE_SCAN_FINGERPRINT) {
|
||||||
|
Log.d(Config.LOGTAG, "Scan OTR fingerprint is not implemented in this version");
|
||||||
|
//new IntentIntegrator(this).initiateScan();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||||||
|
// todo onActivityResult for OTR scan
|
||||||
|
Log.d(Config.LOGTAG, "Scan OTR fingerprint result is not implemented in this version");
|
||||||
|
/*if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) {
|
||||||
|
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
|
||||||
|
if (scanResult != null && scanResult.getFormatName() != null) {
|
||||||
|
String data = scanResult.getContents();
|
||||||
|
XmppUri uri = new XmppUri(data);
|
||||||
|
if (xmppConnectionServiceBound) {
|
||||||
|
verifyWithUri(uri);
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
this.mPendingUri = uri;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
super.onActivityResult(requestCode, requestCode, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBackendConnected() {
|
||||||
|
if (handleIntent(getIntent())) {
|
||||||
|
updateView();
|
||||||
|
} else if (mPendingUri != null) {
|
||||||
|
verifyWithUri(mPendingUri);
|
||||||
|
finish();
|
||||||
|
mPendingUri = null;
|
||||||
|
}
|
||||||
|
setIntent(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateView() {
|
||||||
|
if (this.mConversation != null && this.mConversation.hasValidOtrSession()) {
|
||||||
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
|
this.mVerificationExplain.setText(R.string.no_otr_session_found);
|
||||||
|
invalidateOptionsMenu();
|
||||||
|
switch (this.mode) {
|
||||||
|
case MODE_ASK_QUESTION:
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setTitle(R.string.ask_question);
|
||||||
|
}
|
||||||
|
this.updateViewAskQuestion();
|
||||||
|
break;
|
||||||
|
case MODE_ANSWER_QUESTION:
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setTitle(R.string.smp_requested);
|
||||||
|
}
|
||||||
|
this.updateViewAnswerQuestion();
|
||||||
|
break;
|
||||||
|
case MODE_MANUAL_VERIFICATION:
|
||||||
|
default:
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setTitle(R.string.manually_verify);
|
||||||
|
}
|
||||||
|
this.updateViewManualVerification();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.mManualVerificationArea.setVisibility(View.GONE);
|
||||||
|
this.mSmpVerificationArea.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateViewManualVerification() {
|
||||||
|
this.mVerificationExplain.setText(R.string.manual_verification_explanation);
|
||||||
|
this.mManualVerificationArea.setVisibility(View.VISIBLE);
|
||||||
|
this.mSmpVerificationArea.setVisibility(View.GONE);
|
||||||
|
this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint()));
|
||||||
|
this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint()));
|
||||||
|
if (this.mConversation.isOtrFingerprintVerified()) {
|
||||||
|
deactivateButton(this.mRightButton, R.string.verified);
|
||||||
|
activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
|
||||||
|
} else {
|
||||||
|
activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
|
||||||
|
activateButton(this.mRightButton, R.string.verify, new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
showManuallyVerifyDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateViewAskQuestion() {
|
||||||
|
this.mManualVerificationArea.setVisibility(View.GONE);
|
||||||
|
this.mSmpVerificationArea.setVisibility(View.VISIBLE);
|
||||||
|
this.mVerificationExplain.setText(R.string.smp_explain_question);
|
||||||
|
final int smpStatus = this.mConversation.smp().status;
|
||||||
|
switch (smpStatus) {
|
||||||
|
case Conversation.Smp.STATUS_WE_REQUESTED:
|
||||||
|
this.mStatusMessage.setVisibility(View.GONE);
|
||||||
|
this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
|
||||||
|
this.mSharedSecretSecret.setVisibility(View.VISIBLE);
|
||||||
|
this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint);
|
||||||
|
this.mSharedSecretSecret.setText(this.mConversation.smp().secret);
|
||||||
|
this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener);
|
||||||
|
this.deactivateButton(this.mRightButton, R.string.in_progress);
|
||||||
|
break;
|
||||||
|
case Conversation.Smp.STATUS_FAILED:
|
||||||
|
this.mStatusMessage.setVisibility(View.GONE);
|
||||||
|
this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
|
||||||
|
this.mSharedSecretSecret.setVisibility(View.VISIBLE);
|
||||||
|
this.mSharedSecretSecret.requestFocus();
|
||||||
|
this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match));
|
||||||
|
this.deactivateButton(this.mLeftButton, R.string.cancel);
|
||||||
|
this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener);
|
||||||
|
break;
|
||||||
|
case Conversation.Smp.STATUS_VERIFIED:
|
||||||
|
this.mSharedSecretHintEditable.setText("");
|
||||||
|
this.mSharedSecretHintEditable.setVisibility(View.GONE);
|
||||||
|
this.mSharedSecretSecret.setText("");
|
||||||
|
this.mSharedSecretSecret.setVisibility(View.GONE);
|
||||||
|
this.mStatusMessage.setVisibility(View.VISIBLE);
|
||||||
|
this.deactivateButton(this.mLeftButton, R.string.cancel);
|
||||||
|
this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.mStatusMessage.setVisibility(View.GONE);
|
||||||
|
this.mSharedSecretHintEditable.setVisibility(View.VISIBLE);
|
||||||
|
this.mSharedSecretSecret.setVisibility(View.VISIBLE);
|
||||||
|
this.activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener);
|
||||||
|
this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateViewAnswerQuestion() {
|
||||||
|
this.mManualVerificationArea.setVisibility(View.GONE);
|
||||||
|
this.mSmpVerificationArea.setVisibility(View.VISIBLE);
|
||||||
|
this.mVerificationExplain.setText(R.string.smp_explain_answer);
|
||||||
|
this.mSharedSecretHintEditable.setVisibility(View.GONE);
|
||||||
|
this.mSharedSecretHint.setVisibility(View.VISIBLE);
|
||||||
|
this.deactivateButton(this.mLeftButton, R.string.cancel);
|
||||||
|
final int smpStatus = this.mConversation.smp().status;
|
||||||
|
switch (smpStatus) {
|
||||||
|
case Conversation.Smp.STATUS_CONTACT_REQUESTED:
|
||||||
|
this.mStatusMessage.setVisibility(View.GONE);
|
||||||
|
this.mSharedSecretHint.setText(this.mConversation.smp().hint);
|
||||||
|
this.activateButton(this.mRightButton, R.string.respond, this.mRespondSharedSecretListener);
|
||||||
|
break;
|
||||||
|
case Conversation.Smp.STATUS_VERIFIED:
|
||||||
|
this.mSharedSecretHintEditable.setText("");
|
||||||
|
this.mSharedSecretHintEditable.setVisibility(View.GONE);
|
||||||
|
this.mSharedSecretHint.setVisibility(View.GONE);
|
||||||
|
this.mSharedSecretSecret.setText("");
|
||||||
|
this.mSharedSecretSecret.setVisibility(View.GONE);
|
||||||
|
this.mStatusMessage.setVisibility(View.VISIBLE);
|
||||||
|
this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
|
||||||
|
break;
|
||||||
|
case Conversation.Smp.STATUS_FAILED:
|
||||||
|
default:
|
||||||
|
this.mSharedSecretSecret.requestFocus();
|
||||||
|
this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match));
|
||||||
|
this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void activateButton(Button button, int text, View.OnClickListener listener) {
|
||||||
|
button.setEnabled(true);
|
||||||
|
button.setText(text);
|
||||||
|
button.setOnClickListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void deactivateButton(Button button, int text) {
|
||||||
|
button.setEnabled(false);
|
||||||
|
button.setText(text);
|
||||||
|
button.setOnClickListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_verify_otr);
|
||||||
|
this.mRemoteFingerprint = findViewById(R.id.remote_fingerprint);
|
||||||
|
this.mYourFingerprint = findViewById(R.id.your_fingerprint);
|
||||||
|
this.mLeftButton = findViewById(R.id.left_button);
|
||||||
|
this.mRightButton = findViewById(R.id.right_button);
|
||||||
|
this.mVerificationExplain = findViewById(R.id.verification_explanation);
|
||||||
|
this.mStatusMessage = findViewById(R.id.status_message);
|
||||||
|
this.mSharedSecretSecret = findViewById(R.id.shared_secret_secret);
|
||||||
|
this.mSharedSecretHintEditable = findViewById(R.id.shared_secret_hint_editable);
|
||||||
|
this.mSharedSecretHint = findViewById(R.id.shared_secret_hint);
|
||||||
|
this.mManualVerificationArea = findViewById(R.id.manual_verification_area);
|
||||||
|
this.mSmpVerificationArea = findViewById(R.id.smp_verification_area);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
|
super.onCreateOptionsMenu(menu);
|
||||||
|
getMenuInflater().inflate(R.menu.verify_otr, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showManuallyVerifyDialog() {
|
||||||
|
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
builder.setTitle(R.string.manually_verify);
|
||||||
|
builder.setMessage(R.string.are_you_sure_verify_fingerprint);
|
||||||
|
builder.setNegativeButton(R.string.cancel, null);
|
||||||
|
builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener);
|
||||||
|
builder.create().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getShareableUri() {
|
||||||
|
if (mAccount != null) {
|
||||||
|
return mAccount.getShareableUri();
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onConversationUpdate() {
|
||||||
|
refreshUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void refreshUiReal() {
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,6 +56,8 @@ import androidx.databinding.DataBindingUtil;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
|
import net.java.otr4j.session.SessionID;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -446,7 +448,18 @@ public abstract class XmppActivity extends ActionBarActivity {
|
||||||
|
|
||||||
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
|
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
|
||||||
final Contact contact = conversation.getContact();
|
final Contact contact = conversation.getContact();
|
||||||
if (contact.showInRoster() || contact.isSelf()) {
|
|
||||||
|
if (conversation.hasValidOtrSession()) {
|
||||||
|
SessionID id = conversation.getOtrSession().getSessionID();
|
||||||
|
Jid jid;
|
||||||
|
try {
|
||||||
|
jid = Jid.of(id.getAccountID() + "/" + id.getUserID());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
jid = null;
|
||||||
|
}
|
||||||
|
conversation.setNextCounterpart(jid);
|
||||||
|
listener.onPresenceSelected();
|
||||||
|
} else if (contact.showInRoster() || contact.isSelf()) {
|
||||||
final Presences presences = contact.getPresences();
|
final Presences presences = contact.getPresences();
|
||||||
if (presences.size() == 0) {
|
if (presences.size() == 0) {
|
||||||
if (contact.isSelf()) {
|
if (contact.isSelf()) {
|
||||||
|
|
|
@ -41,6 +41,7 @@ public final class CryptoHelper {
|
||||||
private static final int PW_LENGTH = 12;
|
private static final int PW_LENGTH = 12;
|
||||||
private static final char[] VOWELS = "aeiou".toCharArray();
|
private static final char[] VOWELS = "aeiou".toCharArray();
|
||||||
private static final char[] CONSONANTS = "bcfghjklmnpqrstvwxyz".toCharArray();
|
private static final char[] CONSONANTS = "bcfghjklmnpqrstvwxyz".toCharArray();
|
||||||
|
public static final String FILETRANSFER = "?FILETRANSFERv1:";
|
||||||
private final static char[] hexArray = "0123456789abcdef".toCharArray();
|
private final static char[] hexArray = "0123456789abcdef".toCharArray();
|
||||||
|
|
||||||
public static String bytesToHex(byte[] bytes) {
|
public static String bytesToHex(byte[] bytes) {
|
||||||
|
|
|
@ -609,6 +609,8 @@ public class UIHelper {
|
||||||
} else {
|
} else {
|
||||||
return context.getString(R.string.send_message_to_x, conversation.getName());
|
return context.getString(R.string.send_message_to_x, conversation.getName());
|
||||||
}
|
}
|
||||||
|
case Message.ENCRYPTION_OTR:
|
||||||
|
return context.getString(R.string.send_otr_message);
|
||||||
case Message.ENCRYPTION_AXOLOTL:
|
case Message.ENCRYPTION_AXOLOTL:
|
||||||
AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
|
AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
|
||||||
if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
|
if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {
|
||||||
|
|
|
@ -29,6 +29,7 @@ public class XmppUri {
|
||||||
public static final String PARAMETER_PRE_AUTH = "preauth";
|
public static final String PARAMETER_PRE_AUTH = "preauth";
|
||||||
public static final String PARAMETER_IBR = "ibr";
|
public static final String PARAMETER_IBR = "ibr";
|
||||||
private static final String OMEMO_URI_PARAM = "omemo-sid-";
|
private static final String OMEMO_URI_PARAM = "omemo-sid-";
|
||||||
|
private static final String OTR_URI_PARAM = "otr-fingerprint";
|
||||||
protected Uri uri;
|
protected Uri uri;
|
||||||
protected String jid;
|
protected String jid;
|
||||||
private List<Fingerprint> fingerprints = new ArrayList<>();
|
private List<Fingerprint> fingerprints = new ArrayList<>();
|
||||||
|
@ -111,6 +112,8 @@ public class XmppUri {
|
||||||
if (type == XmppUri.FingerprintType.OMEMO) {
|
if (type == XmppUri.FingerprintType.OMEMO) {
|
||||||
builder.append(XmppUri.OMEMO_URI_PARAM);
|
builder.append(XmppUri.OMEMO_URI_PARAM);
|
||||||
builder.append(fingerprints.get(i).deviceId);
|
builder.append(fingerprints.get(i).deviceId);
|
||||||
|
} else if (type == XmppUri.FingerprintType.OTR) {
|
||||||
|
builder.append(XmppUri.OTR_URI_PARAM);
|
||||||
}
|
}
|
||||||
builder.append('=');
|
builder.append('=');
|
||||||
builder.append(fingerprints.get(i).fingerprint);
|
builder.append(fingerprints.get(i).fingerprint);
|
||||||
|
@ -241,7 +244,8 @@ public class XmppUri {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FingerprintType {
|
public enum FingerprintType {
|
||||||
OMEMO
|
OMEMO,
|
||||||
|
OTR
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Fingerprint {
|
public static class Fingerprint {
|
||||||
|
@ -249,6 +253,10 @@ public class XmppUri {
|
||||||
public final String fingerprint;
|
public final String fingerprint;
|
||||||
final int deviceId;
|
final int deviceId;
|
||||||
|
|
||||||
|
public Fingerprint(FingerprintType type, String fingerprint) {
|
||||||
|
this(type, fingerprint, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
|
public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.fingerprint = fingerprint;
|
this.fingerprint = fingerprint;
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
package eu.siacs.conversations.xmpp.jid;
|
||||||
|
|
||||||
|
import net.java.otr4j.session.SessionID;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
|
public final class OtrJidHelper {
|
||||||
|
|
||||||
|
public static Jid fromSessionID(final SessionID id) throws IllegalArgumentException {
|
||||||
|
if (id.getUserID().isEmpty()) {
|
||||||
|
return Jid.of(id.getAccountID());
|
||||||
|
} else {
|
||||||
|
return Jid.of(id.getAccountID() + "/" + id.getUserID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
src/main/res/layout/activity_verify_otr.xml
Normal file
148
src/main/res/layout/activity_verify_otr.xml
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/color_background_tertiary">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_above="@+id/button_bar">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="@dimen/activity_horizontal_margin"
|
||||||
|
android:layout_marginLeft="@dimen/activity_horizontal_margin"
|
||||||
|
android:layout_marginTop="@dimen/activity_vertical_margin"
|
||||||
|
android:layout_marginEnd="@dimen/activity_horizontal_margin"
|
||||||
|
android:layout_marginRight="@dimen/activity_horizontal_margin"
|
||||||
|
android:layout_marginBottom="@dimen/activity_vertical_margin"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verification_explanation"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/manual_verification_area"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/your_fingerprint"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textAppearance="@style/TextAppearance.Conversations.Body1"
|
||||||
|
android:typeface="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/your_fingerprint"
|
||||||
|
android:textAppearance="@style/TextAppearance.Conversations.Caption" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/remote_fingerprint"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textAppearance="@style/TextAppearance.Conversations.Body1"
|
||||||
|
android:typeface="monospace" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:text="@string/remote_fingerprint"
|
||||||
|
android:textAppearance="@style/TextAppearance.Conversations.Caption" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/smp_verification_area"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:text="@string/verified"
|
||||||
|
android:textAppearance="@style/TextAppearance.Conversations.Title"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/shared_secret_hint"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Conversations.Body1"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<eu.siacs.conversations.ui.widget.TextInputEditText
|
||||||
|
android:id="@+id/shared_secret_hint_editable"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="@string/shared_secret_hint"
|
||||||
|
android:inputType="textAutoComplete"
|
||||||
|
android:textAppearance="@style/TextAppearance.Conversations.Body1"
|
||||||
|
android:textColorHint="?attr/colorAccent" />
|
||||||
|
|
||||||
|
<eu.siacs.conversations.ui.widget.TextInputEditText
|
||||||
|
android:id="@+id/shared_secret_secret"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/shared_secret_secret"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:textAppearance="@style/TextAppearance.Conversations.Body1"
|
||||||
|
android:textColorHint="?attr/colorAccent" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/button_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentBottom="true">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/left_button"
|
||||||
|
style="@style/Widget.Conversations.Button.Borderless"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:layout_marginBottom="7dp"
|
||||||
|
android:background="?attr/color_background_primary" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/right_button"
|
||||||
|
style="@style/Widget.Conversations.Button.Borderless"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
12
src/main/res/menu/verification_choices.xml
Normal file
12
src/main/res/menu/verification_choices.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/ask_question"
|
||||||
|
android:title="@string/ask_question" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/manual_verification"
|
||||||
|
android:title="@string/manually_verify" />
|
||||||
|
|
||||||
|
</menu>
|
15
src/main/res/menu/verify_otr.xml
Normal file
15
src/main/res/menu/verify_otr.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_show_qr_code"
|
||||||
|
android:title="@string/show_qr_code"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
app:showAsAction="never"
|
||||||
|
android:title="@string/action_settings" />
|
||||||
|
</menu>
|
|
@ -60,6 +60,7 @@
|
||||||
\n\nhttps://github.com/zxing/zxing\n(Apache License, Version 2.0)
|
\n\nhttps://github.com/zxing/zxing\n(Apache License, Version 2.0)
|
||||||
\n\nhttps://github.com/osmdroid/osmdroid\n(Apache License, Version 2.0)
|
\n\nhttps://github.com/osmdroid/osmdroid\n(Apache License, Version 2.0)
|
||||||
\n\nhttps://git.singpolyma.net/cheogram-android\n(GPLv3)
|
\n\nhttps://git.singpolyma.net/cheogram-android\n(GPLv3)
|
||||||
|
\n\nhttps://github.com/jitsi/otr4j\n(LGPL-3.0)
|
||||||
\n\n\nMaps
|
\n\n\nMaps
|
||||||
\n\nMaps by Open Street Map (https://www.openstreetmap.org). Copyright restrictions may apply.
|
\n\nMaps by Open Street Map (https://www.openstreetmap.org). Copyright restrictions may apply.
|
||||||
</string>
|
</string>
|
||||||
|
|
|
@ -1142,4 +1142,38 @@
|
||||||
<string name="struck" translatable="false">Struck</string>
|
<string name="struck" translatable="false">Struck</string>
|
||||||
<string name="whisper" translatable="false">Whisper</string>
|
<string name="whisper" translatable="false">Whisper</string>
|
||||||
<string name="lime" translatable="false">Lime</string>
|
<string name="lime" translatable="false">Lime</string>
|
||||||
|
|
||||||
|
<string name="send_otr_message">Write message…</string>
|
||||||
|
<string name="unknown_otr_fingerprint">Unknown OTR fingerprint</string>
|
||||||
|
<string name="otr_fingerprint">OTR fingerprint</string>
|
||||||
|
<string name="otr_fingerprint_selected_message">OTR fingerprint of message</string>
|
||||||
|
<string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string>
|
||||||
|
<string name="verify_otr">Verify OTR</string>
|
||||||
|
<string name="no_otr_session_found">No valid OTR session has been found!</string>
|
||||||
|
<string name="are_you_sure_verify_fingerprint">Are you sure that you want to verify your contacts OTR fingerprint?</string>
|
||||||
|
<string name="copy_otr_clipboard_description">Copy OTR fingerprint to clipboard</string>
|
||||||
|
<string name="otr_session_not_started">Send a message to start an encrypted chat</string>
|
||||||
|
<string name="pref_enable_otr_summary">Enable OTR encryption for message encryption. OTR is still highly unstable, please only use it if you know what you do.</string>
|
||||||
|
<string name="pref_enable_otr">Enable OTR encryption (BETA)</string>
|
||||||
|
<string name="otr_warning">The support of OTR encryption is in the beta mode. Click read more to get more information. A link in a browser will open.</string>
|
||||||
|
<string name="verified">Verified!</string>
|
||||||
|
<string name="smp_requested">Contact requested SMP verification</string>
|
||||||
|
<string name="smp_explain_question">If you and your contact have a secret in common that no one else knows (like an inside joke or simply what you had for lunch the last time you met) you can use that secret to verify each other’s fingerprints.\n\nYou provide a hint or a question for your contact who will respond with a case-sensitive answer.</string>
|
||||||
|
<string name="smp_explain_answer">Your contact would like to verify your fingerprint by challenging you with a shared secret. Your contact provided the following hint or question for that secret.</string>
|
||||||
|
<string name="shared_secret_hint_should_not_be_empty">Your hint should not be empty</string>
|
||||||
|
<string name="shared_secret_can_not_be_empty">Your shared secret can not be empty</string>
|
||||||
|
<string name="manual_verification_explanation">Carefully compare the fingerprint shown below with the fingerprint of your contact.\nYou can use any trusted form of communication like an encrypted e-mail or a telephone call to exchange those.</string>
|
||||||
|
<string name="could_not_verify_fingerprint">Could not verify fingerprint</string>
|
||||||
|
<string name="manually_verify">Manually verify</string>
|
||||||
|
<string name="secrets_do_not_match">Secrets do not match</string>
|
||||||
|
<string name="ask_question">Ask question</string>
|
||||||
|
<string name="verify">Verify</string>
|
||||||
|
<string name="in_progress">In progress</string>
|
||||||
|
<string name="respond">Respond</string>
|
||||||
|
<string name="failed">Failed</string>
|
||||||
|
<string name="finish">Finish</string>
|
||||||
|
<string name="your_fingerprint">Your fingerprint</string>
|
||||||
|
<string name="remote_fingerprint">Remote Fingerprint</string>
|
||||||
|
<string name="shared_secret_hint">Hint or Question</string>
|
||||||
|
<string name="shared_secret_secret">Shared Secret</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue