OTR & another.im #23

Merged
Sergei Poljanski merged 65 commits from develop into master 2024-12-09 18:42:00 +00:00
26 changed files with 1648 additions and 10 deletions
Showing only changes of commit 389074e802 - Show all commits

View file

@ -71,6 +71,7 @@ dependencies {
implementation 'com.google.guava:guava:32.1.3-android'
implementation 'io.michaelrocks:libphonenumber-android:8.13.17'
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.recyclerview:recyclerview:1.2.1"

View file

@ -299,6 +299,11 @@
<activity
android:name=".ui.PublishGroupChatProfilePictureActivity"
android:label="@string/group_chat_avatar" />
<activity
android:name=".ui.VerifyOTRActivity"
android:label="@string/verify_otr"
android:exported="false"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.ShareWithActivity"
android:exported="true"

View file

@ -15,9 +15,10 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
public final class Config {
private static final int UNENCRYPTED = 1;
private static final int OPENPGP = 2;
private static final int OTR = 4;
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() {
return (ENCRYPTION_MASK & UNENCRYPTED) != 0;
@ -31,6 +32,10 @@ public final class Config {
return (ENCRYPTION_MASK & OMEMO) != 0;
}
public static boolean supportOtr() {
return (ENCRYPTION_MASK & OTR) != 0;
}
public static boolean omemoOnly() {
return !multipleEncryptionChoices() && supportOmemo();
}

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

View file

@ -8,20 +8,27 @@ import android.util.Log;
import com.google.common.base.Strings;
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.JSONObject;
import java.security.PublicKey;
import java.security.interfaces.DSAPublicKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
@ -93,6 +100,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
protected String avatar;
protected String hostname = null;
protected int port = 5222;
private OtrService mOtrService = null;
protected boolean online = false;
private String rosterVersion;
private String displayName = null;
@ -100,6 +108,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private PgpDecryptionService pgpDecryptionService = null;
private XmppConnection xmppConnection = null;
private long mEndGracePeriod = 0L;
private String otrFingerprint;
private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
private Presence.Status presenceStatus;
private String presenceStatusMessage;
@ -535,6 +544,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public void initAccountServices(final XmppConnectionService context) {
this.mOtrService = new OtrService(context, this);
this.axolotlService = new AxolotlService(this, context);
this.pgpDecryptionService = new PgpDecryptionService(context);
if (xmppConnection != null) {
@ -542,6 +552,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
}
public OtrService getOtrService() {
return this.mOtrService;
}
public PgpDecryptionService getPgpDecryptionService() {
return this.pgpDecryptionService;
}
@ -554,6 +568,27 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
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() {
if (this.rosterVersion == null) {
return "";
@ -721,6 +756,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private List<XmppUri.Fingerprint> getFingerprints() {
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) {
return fingerprints;
}

View file

@ -348,6 +348,47 @@ public class Contact implements ListItem, Blockable {
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() {
synchronized (this.keys) {
if (this.keys.has("pgp_keyid")) {

View file

@ -71,6 +71,7 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.security.interfaces.DSAPublicKey;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@ -81,6 +82,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.Timer;
@ -132,6 +134,12 @@ import me.saket.bettermovementmethod.BetterLinkMovementMethod;
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 static final String TABLENAME = "conversations";
@ -180,10 +188,15 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
private int mode;
private JSONObject attributes;
private Jid nextCounterpart;
private transient SessionImpl otrSession;
private transient String otrFingerprint = null;
private Smp mSmp = new Smp();
private transient MucOptions mucOptions = null;
private byte[] symmetricKey;
private boolean messagesLeftOnServer = true;
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
private String mLastReceivedOtrMessageId = null;
private String mFirstMamReference = null;
protected Message replyTo = null;
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) {
final ArrayList<Message> results = new ArrayList<>();
synchronized (this.messages) {
@ -662,6 +686,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return getContact().getBlockedJid();
}
public String getLastReceivedOtrMessageId() {
return this.mLastReceivedOtrMessageId;
}
public void setLastReceivedOtrMessageId(String id) {
this.mLastReceivedOtrMessageId = id;
}
public int countMessages() {
synchronized (this.messages) {
return this.messages.size();
@ -923,6 +955,124 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
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
*/
@ -964,7 +1114,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
public int getNextEncryption() {
if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
if (!Config.supportOmemo() && !Config.supportOpenPgp() && !Config.supportOtr()) {
return Message.ENCRYPTION_NONE;
}
if (OmemoSetting.isAlways()) {
@ -993,6 +1143,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return nextMessage == null ? "" : nextMessage;
}
public boolean smpRequested() {
return smp().status == Smp.STATUS_CONTACT_REQUESTED;
}
public @Nullable
Draft getDraft() {
long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
@ -1015,6 +1169,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return changed;
}
public void setSymmetricKey(byte[] key) {
this.symmetricKey = key;
}
public byte[] getSymmetricKey() {
return this.symmetricKey;
}
public Bookmark getBookmark() {
return this.account.getBookmark(this.contactJid);
}

View file

@ -6,7 +6,7 @@ import java.util.List;
public interface Transferable {
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_CHECKING = 0x201;

View file

@ -57,6 +57,9 @@ public abstract class AbstractGenerator {
private final String[] PRIVACY_SENSITIVE = {
"urn:xmpp:time" //XEP-0202: Entity Time leaks time zone
};
private final String[] OTR = {
"urn:xmpp:otr:0"
};
private final String[] VOIP_NAMESPACES = {
Namespace.JINGLE_TRANSPORT_ICE_UDP,
Namespace.JINGLE_FEATURE_AUDIO,
@ -125,6 +128,9 @@ public abstract class AbstractGenerator {
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
features.addAll(Arrays.asList(VOIP_NAMESPACES));
}
if (Config.supportOtr()) {
features.addAll(Arrays.asList(OTR));
}
if (mXmppConnectionService.broadcastLastActivity()) {
features.add(Namespace.IDLE);
}

View file

@ -1,5 +1,8 @@
package eu.siacs.conversations.generator;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@ -24,6 +27,7 @@ import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
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 doesnt seem to support that";
private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesnt 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 doesnt seem to support that.";
@ -102,6 +106,36 @@ public class MessageGenerator extends AbstractGenerator {
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) {
MessagePacket packet = preparePacket(message);
String content;
@ -233,6 +267,19 @@ public class MessageGenerator extends AbstractGenerator {
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) {
final MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those

View file

@ -1,8 +1,13 @@
package eu.siacs.conversations.parser;
import android.os.Build;
import android.text.Html;
import android.util.Log;
import android.util.Pair;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@ -16,6 +21,7 @@ import java.util.UUID;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.BrokenSessionException;
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.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.ReceiptRequest;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.QuickConversationsService;
@ -49,6 +57,7 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
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);
@ -95,6 +104,30 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
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) {
ChatState state = ChatState.parse(packet);
if (state != null && c != null) {
@ -126,6 +159,66 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
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) {
final AxolotlService service = conversation.getAccount().getAxolotlService();
final XmppAxolotlMessage xmppAxolotlMessage;
@ -327,6 +420,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
final Jid from = packet.getFrom();
final String id = packet.getId();
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)) {
final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
mXmppConnectionService.getJingleConnectionManager()
@ -335,8 +433,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) {
final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length());
final String message = extractErrorMessage(packet);
mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, message);
final String errorMessage = extractErrorMessage(packet);
mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId, errorMessage);
return true;
}
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;
}
@ -368,6 +473,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
final MessagePacket packet;
Long timestamp = null;
final boolean isForwarded;
boolean isCarbon = false;
String serverMsgId = null;
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;
packet = f.first;
isForwarded = true;
serverMsgId = result.getAttribute("id");
query.incrementMessageCount();
if (handleErrorMessage(account, packet)) {
return;
@ -403,8 +511,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
timestamp = f != null ? f.second : null;
isCarbon = f != null;
isForwarded = isCarbon;
} else {
packet = original;
isForwarded = false;
}
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");
return;
}
boolean isProperlyAddressed = (to != null) && (!to.isBareJid() || account.countPresences() == 0);
boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
boolean selfAddressed;
if (packet.fromAccount(account)) {
@ -547,7 +658,20 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
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);
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
Jid origin;
@ -796,6 +920,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
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);
final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) {

View file

@ -61,6 +61,11 @@ import com.google.common.base.Objects;
import com.google.common.base.Optional;
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.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
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.chatstate.ChatState;
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.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
@ -255,9 +261,18 @@ public class XmppConnectionService extends Service {
Conversation conversation = find(getConversations(), contact);
if (conversation != null) {
if (online) {
conversation.endOtrIfNeeded();
if (contact.getPresences().size() == 1) {
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
&& !pendingJoin
&& !inProgressJoin) {
if (!conversation.startOtrIfNeeded()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": couldn't start OTR with " + conversation.getContact().getJid() + " when needed");
}
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);
@ -1736,6 +1760,30 @@ public class XmppConnectionService extends Service {
packet = mMessageGenerator.generatePgpChat(message);
}
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:
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
if (message.needsUploading()) {
@ -1789,6 +1837,12 @@ public class XmppConnectionService extends Service {
}
}
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:
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
break;
@ -3922,6 +3976,12 @@ public class XmppConnectionService extends Service {
if (conversation.getAccount() == account) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
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);
}
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) {
pushContactToServer(contact, null);
}
@ -4503,6 +4596,7 @@ public class XmppConnectionService extends Service {
return false;
} else {
final Message message = conversation.findSentMessageWithUuid(uuid);
if (message != null) {
if (message.getServerMsgId() == null) {
message.setServerMsgId(serverMessageId);
@ -4805,6 +4899,11 @@ public class XmppConnectionService extends Service {
setMemorizingTrustManager(tm);
}
public void syncRosterToDisk(final Account account) {
Runnable runnable = () -> databaseBackend.writeRoster(account.getRoster());
mDatabaseWriterExecutor.execute(runnable);
}
public LruCache<String, Bitmap> getBitmapCache() {
return this.mBitmapCache;
}
@ -5272,10 +5371,14 @@ public class XmppConnectionService extends Service {
}
public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
boolean needsRosterWrite = false;
boolean performedVerification = false;
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
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", "");
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
if (fingerprintStatus != null) {
@ -5288,6 +5391,11 @@ public class XmppConnectionService extends Service {
}
}
}
if (needsRosterWrite) {
syncRosterToDisk(contact.getAccount());
}
return performedVerification;
}

View file

@ -79,6 +79,8 @@ import androidx.viewpager.widget.PagerAdapter;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import net.java.otr4j.session.SessionStatus;
import org.jetbrains.annotations.NotNull;
import java.io.File;
@ -223,6 +225,14 @@ public class ConversationFragment extends XmppFragment
private ConversationsActivity activity;
private Vibrator vibrator;
private boolean reInitRequiredOnStart = true;
protected OnClickListener clickToVerify = new OnClickListener() {
@Override
public void onClick(View v) {
activity.verifyOtrSessionDialog(conversation, v);
}
};
@ColorInt
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 =
new OnClickListener() {
@ -1064,6 +1088,8 @@ public class ConversationFragment extends XmppFragment
message.setUuid(UUID.randomUUID().toString());
}
switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_OTR:
sendOtrMessage(message);
case Message.ENCRYPTION_PGP:
sendPgpMessage(message);
break;
@ -3550,6 +3576,14 @@ public class ConversationFragment extends XmppFragment
}
} else if (account.hasPendingPgpIntent(conversation)) {
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
&& connection.getFeatures().blocking()
&& conversation.countMessages() != 0
@ -3650,6 +3684,11 @@ public class ConversationFragment extends XmppFragment
new Handler()
.post(
() -> {
if (conversation.isInHistoryPart()) {
conversation.jumpToLatest();
refresh(false);
}
int size = messageList.size();
this.binding.messagesView.setSelection(size - 1);
});
@ -3937,6 +3976,17 @@ public class ConversationFragment extends XmppFragment
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) {
final XmppConnectionService xmppService = activity.xmppConnectionService;
final Contact contact = message.getConversation().getContact();

View file

@ -63,9 +63,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityCompat;
import androidx.databinding.DataBindingUtil;
import net.java.otr4j.session.SessionStatus;
import com.google.android.material.bottomnavigation.BottomNavigationView;
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.OnUpdateBlocklist;
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 {
@ -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
public void onConversationArchived(Conversation conversation) {
if (performRedirectIfNecessary(conversation, false)) {

View file

@ -265,7 +265,6 @@ public class SendLogActivity extends ActionBarActivity {
log.insert(0, mAdditonalInfo);
}
android.util.Log.e("35fd", log.toString());
writer.write(log.toString());
}
catch (IOException e){

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

View file

@ -56,6 +56,8 @@ import androidx.databinding.DataBindingUtil;
import com.google.common.base.Strings;
import net.java.otr4j.session.SessionID;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@ -446,7 +448,18 @@ public abstract class XmppActivity extends ActionBarActivity {
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
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();
if (presences.size() == 0) {
if (contact.isSelf()) {

View file

@ -41,6 +41,7 @@ public final class CryptoHelper {
private static final int PW_LENGTH = 12;
private static final char[] VOWELS = "aeiou".toCharArray();
private static final char[] CONSONANTS = "bcfghjklmnpqrstvwxyz".toCharArray();
public static final String FILETRANSFER = "?FILETRANSFERv1:";
private final static char[] hexArray = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes) {

View file

@ -609,6 +609,8 @@ public class UIHelper {
} else {
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:
AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
if (axolotlService != null && axolotlService.trustedSessionVerified(conversation)) {

View file

@ -29,6 +29,7 @@ public class XmppUri {
public static final String PARAMETER_PRE_AUTH = "preauth";
public static final String PARAMETER_IBR = "ibr";
private static final String OMEMO_URI_PARAM = "omemo-sid-";
private static final String OTR_URI_PARAM = "otr-fingerprint";
protected Uri uri;
protected String jid;
private List<Fingerprint> fingerprints = new ArrayList<>();
@ -111,6 +112,8 @@ public class XmppUri {
if (type == XmppUri.FingerprintType.OMEMO) {
builder.append(XmppUri.OMEMO_URI_PARAM);
builder.append(fingerprints.get(i).deviceId);
} else if (type == XmppUri.FingerprintType.OTR) {
builder.append(XmppUri.OTR_URI_PARAM);
}
builder.append('=');
builder.append(fingerprints.get(i).fingerprint);
@ -241,7 +244,8 @@ public class XmppUri {
}
public enum FingerprintType {
OMEMO
OMEMO,
OTR
}
public static class Fingerprint {
@ -249,6 +253,10 @@ public class XmppUri {
public final String fingerprint;
final int deviceId;
public Fingerprint(FingerprintType type, String fingerprint) {
this(type, fingerprint, 0);
}
public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
this.type = type;
this.fingerprint = fingerprint;

View file

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

View 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>

View 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>

View 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>

View file

@ -60,6 +60,7 @@
\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://git.singpolyma.net/cheogram-android\n(GPLv3)
\n\nhttps://github.com/jitsi/otr4j\n(LGPL-3.0)
\n\n\nMaps
\n\nMaps by Open Street Map (https://www.openstreetmap.org). Copyright restrictions may apply.
</string>

View file

@ -1142,4 +1142,38 @@
<string name="struck" translatable="false">Struck</string>
<string name="whisper" translatable="false">Whisper</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 others 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>