Merge branch 'CryptoNextBeta' into development

* CryptoNextBeta: (60 commits)
  Lock TrustKeys if no trusted keys are available
  Optimize imports
  Use MD style for key trust toggle switch
  Fix set/remove OnUpdateBlocklistListener
  Fix axolotl database migration
  Remove device list from EditAccount
  Add clear devices to overflow menu in EditAccount
  Ask for key trust when sending messages
  Encrypt files for HTTP upload in encrypted chats
  Refactor trust key ui and show in account details
  Send correct body for HTTP files
  Handle file transmission properly in axolotl
  Remove unneccessary code
  Fix trust status for outgoing messages
  Don't merge messages with different trust statuses
  Fix copying of axolotl keys to clipboard
  Add refresh icon to v21 theme
  Disable Axolotl option if not usable
  Show trust status of messages' originating session
  Add key trust toggle to ContactDetailsActivity
  ...
This commit is contained in:
Andreas Straub 2015-07-20 15:13:14 +02:00
commit aaa8fe8978
82 changed files with 3323 additions and 316 deletions

View File

@ -37,6 +37,8 @@ dependencies {
compile 'de.timroes.android:EnhancedListView:0.3.4'
compile 'me.leolin:ShortcutBadger:1.1.1@aar'
compile 'com.kyleduo.switchbutton:library:1.2.8'
compile 'org.whispersystems:axolotl-android:1.3.4'
compile 'com.kyleduo.switchbutton:library:1.2.8'
}
android {

View File

@ -130,6 +130,10 @@
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name=".ui.TrustKeysActivity"
android:label="@string/trust_keys"
android:windowSoftInputMode="stateAlwaysHidden"/>
<activity
android:name="de.duenndns.ssl.MemorizingActivity"
android:theme="@style/ConversationsTheme"

View File

@ -1,5 +1,20 @@
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;
@ -11,31 +26,15 @@ import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
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.InstanceTag;
import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.FragmenterInstructions;
public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
private Account account;

View File

@ -1,5 +1,13 @@
package eu.siacs.conversations.crypto;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
@ -9,10 +17,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@ -22,9 +26,6 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
public class PgpEngine {
private OpenPgpApi api;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
package eu.siacs.conversations.crypto.axolotl;
public class NoSessionsCreatedException extends Throwable{
}

View File

@ -0,0 +1,194 @@
package eu.siacs.conversations.crypto.axolotl;
import android.util.Base64;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
public class XmppAxolotlMessage {
private byte[] innerKey;
private byte[] ciphertext;
private byte[] iv;
private final Set<XmppAxolotlMessageHeader> headers;
private final Jid from;
private final int sourceDeviceId;
public static class XmppAxolotlMessageHeader {
private final int recipientDeviceId;
private final byte[] content;
public XmppAxolotlMessageHeader(int deviceId, byte[] content) {
this.recipientDeviceId = deviceId;
this.content = content;
}
public XmppAxolotlMessageHeader(Element header) {
if("header".equals(header.getName())) {
this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid"));
this.content = Base64.decode(header.getContent(),Base64.DEFAULT);
} else {
throw new IllegalArgumentException("Argument not a <header> Element!");
}
}
public int getRecipientDeviceId() {
return recipientDeviceId;
}
public byte[] getContents() {
return content;
}
public Element toXml() {
Element headerElement = new Element("header");
// TODO: generate XML
headerElement.setAttribute("rid", getRecipientDeviceId());
headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT));
return headerElement;
}
}
public static class XmppAxolotlPlaintextMessage {
private final AxolotlService.XmppAxolotlSession session;
private final String plaintext;
private final String fingerprint;
public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) {
this.session = session;
this.plaintext = plaintext;
this.fingerprint = fingerprint;
}
public String getPlaintext() {
return plaintext;
}
public AxolotlService.XmppAxolotlSession getSession() {
return session;
}
public String getFingerprint() {
return fingerprint;
}
}
public XmppAxolotlMessage(Jid from, Element axolotlMessage) {
this.from = from;
this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id"));
this.headers = new HashSet<>();
for(Element child:axolotlMessage.getChildren()) {
switch(child.getName()) {
case "header":
headers.add(new XmppAxolotlMessageHeader(child));
break;
case "message":
iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT);
ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT);
break;
default:
break;
}
}
}
public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) {
this.from = from;
this.sourceDeviceId = sourceDeviceId;
this.headers = new HashSet<>();
this.encrypt(plaintext);
}
private void encrypt(String plaintext) {
try {
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(128);
SecretKey secretKey = generator.generateKey();
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
this.innerKey = secretKey.getEncoded();
this.iv = cipher.getIV();
this.ciphertext = cipher.doFinal(plaintext.getBytes());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| IllegalBlockSizeException | BadPaddingException e) {
}
}
public Jid getFrom() {
return this.from;
}
public int getSenderDeviceId() {
return sourceDeviceId;
}
public byte[] getCiphertext() {
return ciphertext;
}
public Set<XmppAxolotlMessageHeader> getHeaders() {
return headers;
}
public void addHeader(XmppAxolotlMessageHeader header) {
headers.add(header);
}
public byte[] getInnerKey(){
return innerKey;
}
public byte[] getIV() {
return this.iv;
}
public Element toXml() {
// TODO: generate outer XML, add in header XML
Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX);
message.setAttribute("id", sourceDeviceId);
for(XmppAxolotlMessageHeader header: headers) {
message.addChild(header.toXml());
}
Element payload = message.addChild("message");
payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT));
payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT));
return message;
}
public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) {
XmppAxolotlPlaintextMessage plaintextMessage = null;
try {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
String plaintext = new String(cipher.doFinal(ciphertext));
plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException
| BadPaddingException e) {
throw new AssertionError(e);
}
return plaintextMessage;
}
}

View File

@ -20,6 +20,7 @@ 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.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
@ -122,6 +123,7 @@ public class Account extends AbstractEntity {
protected String avatar;
protected boolean online = false;
private OtrService mOtrService = null;
private AxolotlService axolotlService = null;
private XmppConnection xmppConnection = null;
private long mEndGracePeriod = 0L;
private String otrFingerprint;
@ -254,6 +256,10 @@ public class Account extends AbstractEntity {
return keys;
}
public String getKey(final String name) {
return this.keys.optString(name, null);
}
public boolean setKey(final String keyName, final String keyValue) {
try {
this.keys.put(keyName, keyValue);
@ -277,8 +283,13 @@ public class Account extends AbstractEntity {
return values;
}
public AxolotlService getAxolotlService() {
return axolotlService;
}
public void initAccountServices(final XmppConnectionService context) {
this.mOtrService = new OtrService(context, this);
this.axolotlService = new AxolotlService(this, context);
}
public OtrService getOtrService() {

View File

@ -183,6 +183,7 @@ public class Contact implements ListItem, Blockable {
}
public ContentValues getContentValues() {
synchronized (this.keys) {
final ContentValues values = new ContentValues();
values.put(ACCOUNT, accountUuid);
values.put(SYSTEMNAME, systemName);
@ -198,6 +199,7 @@ public class Contact implements ListItem, Blockable {
values.put(GROUPS, groups.toString());
return values;
}
}
public int getSubscription() {
return this.subscription;
@ -281,6 +283,7 @@ public class Contact implements ListItem, Blockable {
}
public ArrayList<String> getOtrFingerprints() {
synchronized (this.keys) {
final ArrayList<String> fingerprints = new ArrayList<String>();
try {
if (this.keys.has("otr_fingerprints")) {
@ -297,8 +300,9 @@ public class Contact implements ListItem, Blockable {
}
return fingerprints;
}
}
public boolean addOtrFingerprint(String print) {
synchronized (this.keys) {
if (getOtrFingerprints().contains(print)) {
return false;
}
@ -306,7 +310,6 @@ public class Contact implements ListItem, Blockable {
JSONArray fingerprints;
if (!this.keys.has("otr_fingerprints")) {
fingerprints = new JSONArray();
} else {
fingerprints = this.keys.getJSONArray("otr_fingerprints");
}
@ -317,8 +320,10 @@ public class Contact implements ListItem, Blockable {
return false;
}
}
}
public long getPgpKeyId() {
synchronized (this.keys) {
if (this.keys.has("pgp_keyid")) {
try {
return this.keys.getLong("pgp_keyid");
@ -329,12 +334,14 @@ public class Contact implements ListItem, Blockable {
return 0;
}
}
}
public void setPgpKeyId(long keyId) {
synchronized (this.keys) {
try {
this.keys.put("pgp_keyid", keyId);
} catch (final JSONException ignored) {
}
}
}
@ -441,6 +448,7 @@ public class Contact implements ListItem, Blockable {
}
public boolean deleteOtrFingerprint(String fingerprint) {
synchronized (this.keys) {
boolean success = false;
try {
if (this.keys.has("otr_fingerprints")) {
@ -461,6 +469,7 @@ public class Contact implements ListItem, Blockable {
return false;
}
}
}
public boolean trusted() {
return getOption(Options.FROM) && getOption(Options.TO);

View File

@ -179,11 +179,11 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) {
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() == Message.ENCRYPTION_OTR)) {
&& (message.getEncryption() == encryptionType)) {
onMessageFound.onMessageFound(message);
}
}

View File

@ -1,12 +1,13 @@
package eu.siacs.conversations.entities;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLConnection;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
@ -22,8 +23,6 @@ import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.MimeUtils;
import android.util.Log;
public class DownloadableFile extends File {
private static final long serialVersionUID = 2247012619505115863L;

View File

@ -8,6 +8,7 @@ import java.net.URL;
import java.util.Arrays;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
@ -34,6 +35,7 @@ public class Message extends AbstractEntity {
public static final int ENCRYPTION_OTR = 2;
public static final int ENCRYPTION_DECRYPTED = 3;
public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
public static final int ENCRYPTION_AXOLOTL = 5;
public static final int TYPE_TEXT = 0;
public static final int TYPE_IMAGE = 1;
@ -52,6 +54,7 @@ public class Message extends AbstractEntity {
public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
public static final String FINGERPRINT = "axolotl_fingerprint";
public static final String ME_COMMAND = "/me ";
@ -73,6 +76,7 @@ public class Message extends AbstractEntity {
protected Transferable transferable = null;
private Message mNextMessage = null;
private Message mPreviousMessage = null;
private String axolotlFingerprint = null;
private Message() {
@ -94,6 +98,7 @@ public class Message extends AbstractEntity {
TYPE_TEXT,
null,
null,
null,
null);
this.conversation = conversation;
}
@ -101,7 +106,7 @@ public class Message extends AbstractEntity {
private Message(final String uuid, final String conversationUUid, final Jid counterpart,
final Jid trueCounterpart, final String body, final long timeSent,
final int encryption, final int status, final int type, final String remoteMsgId,
final String relativeFilePath, final String serverMsgId) {
final String relativeFilePath, final String serverMsgId, final String fingerprint) {
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
@ -114,6 +119,7 @@ public class Message extends AbstractEntity {
this.remoteMsgId = remoteMsgId;
this.relativeFilePath = relativeFilePath;
this.serverMsgId = serverMsgId;
this.axolotlFingerprint = fingerprint;
}
public static Message fromCursor(Cursor cursor) {
@ -150,7 +156,8 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(TYPE)),
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndex(FINGERPRINT)));
}
public static Message createStatusMessage(Conversation conversation, String body) {
@ -184,6 +191,7 @@ public class Message extends AbstractEntity {
values.put(REMOTE_MSG_ID, remoteMsgId);
values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId);
values.put(FINGERPRINT, axolotlFingerprint);
return values;
}
@ -391,7 +399,8 @@ public class Message extends AbstractEntity {
!message.getBody().startsWith(ME_COMMAND) &&
!this.getBody().startsWith(ME_COMMAND) &&
!this.bodyIsHeart() &&
!message.bodyIsHeart()
!message.bodyIsHeart() &&
this.isTrusted() == message.isTrusted()
);
}
@ -663,4 +672,17 @@ public class Message extends AbstractEntity {
public int width = 0;
public int height = 0;
}
public void setAxolotlFingerprint(String fingerprint) {
this.axolotlFingerprint = fingerprint;
}
public String getAxolotlFingerprint() {
return axolotlFingerprint;
}
public boolean isTrusted() {
return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint)
== AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED;
}
}

View File

@ -1,5 +1,7 @@
package eu.siacs.conversations.entities;
import android.annotation.SuppressLint;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@ -11,8 +13,6 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
import android.annotation.SuppressLint;
@SuppressLint("DefaultLocale")
public class MucOptions {

View File

@ -12,6 +12,7 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.PhoneHelper;
@ -28,7 +29,8 @@ public abstract class AbstractGenerator {
"urn:xmpp:avatar:metadata+notify",
"urn:xmpp:ping",
"jabber:iq:version",
"http://jabber.org/protocol/chatstates"};
"http://jabber.org/protocol/chatstates",
AxolotlService.PEP_DEVICE_LIST+"+notify"};
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
"urn:xmpp:chat-markers:0",
"urn:xmpp:receipts"

View File

@ -1,15 +1,23 @@
package eu.siacs.conversations.generator;
import android.util.Base64;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.forms.Data;
@ -115,6 +123,56 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket retrieveDeviceIds(final Jid to) {
final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
if(to != null) {
packet.setTo(to);
}
return packet;
}
public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null);
if(to != null) {
packet.setTo(to);
}
return packet;
}
public IqPacket publishDeviceIds(final Set<Integer> ids) {
final Element item = new Element("item");
final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
for(Integer id:ids) {
final Element device = new Element("device");
device.setAttribute("id", id);
list.addChild(device);
}
return publish(AxolotlService.PEP_DEVICE_LIST, item);
}
public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
final Set<PreKeyRecord> preKeyRecords, final int deviceId) {
final Element item = new Element("item");
final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT));
final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT));
final Element identityKeyElement = bundle.addChild("identityKey");
identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
for(PreKeyRecord preKeyRecord:preKeyRecords) {
final Element prekey = prekeys.addChild("preKeyPublic");
prekey.setAttribute("preKeyId", preKeyRecord.getId());
prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT));
}
return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item);
}
public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
final Element query = packet.query("urn:xmpp:mam:0");

View File

@ -1,12 +1,18 @@
package eu.siacs.conversations.generator;
import android.util.Log;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
@ -59,6 +65,22 @@ public class MessageGenerator extends AbstractGenerator {
delay.setAttribute("stamp", mDateFormat.format(date));
}
public MessagePacket generateAxolotlChat(Message message) {
return generateAxolotlChat(message, false);
}
public MessagePacket generateAxolotlChat(Message message, boolean addDelay) {
MessagePacket packet = preparePacket(message, addDelay);
AxolotlService service = message.getConversation().getAccount().getAxolotlService();
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(message.getConversation().getAccount())+"Submitting message to axolotl service for send processing...");
XmppAxolotlMessage axolotlMessage = service.encrypt(message);
if (axolotlMessage == null) {
return null;
}
packet.setAxolotlMessage(axolotlMessage.toXml());
return packet;
}
public MessagePacket generateOtrChat(Message message) {
return generateOtrChat(message, false);
}

View File

@ -18,9 +18,9 @@ import javax.net.ssl.SSLHandshakeException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
@ -70,7 +70,8 @@ public class HttpDownloadConnection implements Transferable {
String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
this.message.setEncryption(Message.ENCRYPTION_PGP);
} else if (message.getEncryption() != Message.ENCRYPTION_OTR) {
} else if (message.getEncryption() != Message.ENCRYPTION_OTR
&& message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
String extension;
@ -86,7 +87,8 @@ public class HttpDownloadConnection implements Transferable {
this.file.setKey(CryptoHelper.hexToBytes(reference));
}
if (this.message.getEncryption() == Message.ENCRYPTION_OTR
if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
|| this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
&& this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}

View File

@ -14,9 +14,9 @@ import javax.net.ssl.HttpsURLConnection;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
@ -88,7 +88,9 @@ public class HttpUploadConnection implements Transferable {
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
this.file.setExpectedSize(this.file.getSize());
if (Config.ENCRYPT_ON_HTTP_UPLOADED) {
if (Config.ENCRYPT_ON_HTTP_UPLOADED
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|| message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[48];
mXmppConnectionService.getRNG().nextBytes(this.key);
this.file.setKey(this.key);

View File

@ -11,7 +11,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
public abstract class AbstractParser {

View File

@ -1,11 +1,25 @@
package eu.siacs.conversations.parser;
import android.support.annotation.NonNull;
import android.util.Base64;
import android.util.Log;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
@ -71,6 +85,155 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return super.avatarData(items);
}
public Element getItem(final IqPacket packet) {
final Element pubsub = packet.findChild("pubsub",
"http://jabber.org/protocol/pubsub");
if (pubsub == null) {
return null;
}
final Element items = pubsub.findChild("items");
if (items == null) {
return null;
}
return items.findChild("item");
}
@NonNull
public Set<Integer> deviceIds(final Element item) {
Set<Integer> deviceIds = new HashSet<>();
if (item != null) {
final Element list = item.findChild("list");
if (list != null) {
for (Element device : list.getChildren()) {
if (!device.getName().equals("device")) {
continue;
}
try {
Integer id = Integer.valueOf(device.getAttribute("id"));
deviceIds.add(id);
} catch (NumberFormatException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered nvalid <device> node in PEP:" + device.toString()
+ ", skipping...");
continue;
}
}
}
}
return deviceIds;
}
public Integer signedPreKeyId(final Element bundle) {
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if(signedPreKeyPublic == null) {
return null;
}
return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
}
public ECPublicKey signedPreKeyPublic(final Element bundle) {
ECPublicKey publicKey = null;
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if(signedPreKeyPublic == null) {
return null;
}
try {
publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage());
}
return publicKey;
}
public byte[] signedPreKeySignature(final Element bundle) {
final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
if(signedPreKeySignature == null) {
return null;
}
return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT);
}
public IdentityKey identityKey(final Element bundle) {
IdentityKey identityKey = null;
final Element identityKeyElement = bundle.findChild("identityKey");
if(identityKeyElement == null) {
return null;
}
try {
identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
}
return identityKey;
}
public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
Element item = getItem(packet);
if (item == null) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet);
return null;
}
final Element bundleElement = item.findChild("bundle");
if(bundleElement == null) {
return null;
}
final Element prekeysElement = bundleElement.findChild("prekeys");
if(prekeysElement == null) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet);
return null;
}
for(Element preKeyPublicElement : prekeysElement.getChildren()) {
if(!preKeyPublicElement.getName().equals("preKeyPublic")){
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
continue;
}
Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
try {
ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
preKeyRecords.put(preKeyId, preKeyPublic);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
continue;
}
}
return preKeyRecords;
}
public PreKeyBundle bundle(final IqPacket bundle) {
Element bundleItem = getItem(bundle);
if(bundleItem == null) {
return null;
}
final Element bundleElement = bundleItem.findChild("bundle");
if(bundleElement == null) {
return null;
}
ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
Integer signedPreKeyId = signedPreKeyId(bundleElement);
byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
IdentityKey identityKey = identityKey(bundleElement);
if(signedPreKeyPublic == null || identityKey == null) {
return null;
}
return new PreKeyBundle(0, 0, 0, null,
signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
}
public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
List<PreKeyBundle> bundles = new ArrayList<>();
Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
if ( preKeyPublics != null) {
for (Integer preKeyId : preKeyPublics.keySet()) {
ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
0, null, null, null));
}
}
return bundles;
}
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {

View File

@ -6,7 +6,11 @@ import android.util.Pair;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -94,6 +98,20 @@ public class MessageParser extends AbstractParser implements
}
}
private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
Message finishedMessage = null;
AxolotlService service = conversation.getAccount().getAxolotlService();
XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(from.toBareJid(), axolotlMessage);
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage);
if(plaintextMessage != null) {
finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
}
return finishedMessage;
}
private class Invite {
Jid jid;
String password;
@ -170,6 +188,13 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateAccountUi();
}
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
Element item = items.findChild("item");
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
AxolotlService axolotlService = account.getAxolotlService();
axolotlService.registerDevices(from, deviceIds);
mXmppConnectionService.updateAccountUi();
}
}
@ -232,8 +257,9 @@ public class MessageParser extends AbstractParser implements
timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
}
final String body = packet.getBody();
final String encrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user");
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
final Jid to = packet.getTo();
@ -261,11 +287,11 @@ public class MessageParser extends AbstractParser implements
return;
}
if (extractChatState(mXmppConnectionService.find(account,from), packet)) {
if (extractChatState(mXmppConnectionService.find(account, from), packet)) {
mXmppConnectionService.updateConversationUi();
}
if ((body != null || encrypted != null) && !isMucStatusMessage) {
if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat);
if (isTypeGroupChat) {
if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
@ -294,8 +320,13 @@ public class MessageParser extends AbstractParser implements
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
} else if (encrypted != null) {
message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status);
} else if (pgpEncrypted != null) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null) {
message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
if (message == null) {
return;
}
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}

View File

@ -1,10 +1,32 @@
package eu.siacs.conversations.persistance;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Base64;
import android.util.Log;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -13,19 +35,12 @@ import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 14;
private static final int DATABASE_VERSION = 15;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -39,6 +54,60 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ Contact.JID + ") ON CONFLICT REPLACE);";
private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.ID
+ ") ON CONFLICT REPLACE"
+");";
private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.ID
+ ") ON CONFLICT REPLACE"+
");";
private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.NAME + ", "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID
+ ") ON CONFLICT REPLACE"
+");";
private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE "
+ AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME + "("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ AxolotlService.SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", "
+ AxolotlService.SQLiteAxolotlStore.NAME + ", "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT
+ ") ON CONFLICT IGNORE"
+");";
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@ -69,12 +138,17 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
+ Message.RELATIVE_FILE_PATH + " TEXT, "
+ Message.SERVER_MSG_ID + " TEXT, "
+ Message.FINGERPRINT + " TEXT, "
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
+ ") ON DELETE CASCADE);");
db.execSQL(CREATE_CONTATCS_STATEMENT);
db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
}
@Override
@ -215,6 +289,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
cursor.close();
}
if (oldVersion < 15 && newVersion >= 15) {
recreateAxolotlDb(db);
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.FINGERPRINT + " TEXT");
}
}
public static synchronized DatabaseBackend getInstance(Context context) {
@ -311,7 +390,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
};
Cursor cursor = db.query(Conversation.TABLENAME, null,
Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
+ " like ? OR "+Conversation.CONTACTJID+"=?)", selectionArgs, null, null, null);
+ " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
if (cursor.getCount() == 0)
return null;
cursor.moveToFirst();
@ -481,4 +560,403 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.close();
return list;
}
private Cursor getCursorForSession(Account account, AxolotlAddress contact) {
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = null;
String[] selectionArgs = {account.getUuid(),
contact.getName(),
Integer.toString(contact.getDeviceId())};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ",
selectionArgs,
null, null, null);
return cursor;
}
public SessionRecord loadSession(Account account, AxolotlAddress contact) {
SessionRecord session = null;
Cursor cursor = getCursorForSession(account, contact);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e) {
cursor.close();
throw new AssertionError(e);
}
}
cursor.close();
return session;
}
public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) {
List<Integer> devices = new ArrayList<>();
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.DEVICE_ID};
String[] selectionArgs = {account.getUuid(),
contact.getName()};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ?",
selectionArgs,
null, null, null);
while(cursor.moveToNext()) {
devices.add(cursor.getInt(
cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.DEVICE_ID)));
}
cursor.close();
return devices;
}
public boolean containsSession(Account account, AxolotlAddress contact) {
Cursor cursor = getCursorForSession(account, contact);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName());
values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
}
public void deleteSession(Account account, AxolotlAddress contact) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(),
contact.getName(),
Integer.toString(contact.getDeviceId())};
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ",
args);
}
public void deleteAllSessions(Account account, AxolotlAddress contact) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), contact.getName()};
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.NAME + " = ?",
args);
}
private Cursor getCursorForPreKey(Account account, int preKeyId) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?",
selectionArgs,
null, null, null);
return cursor;
}
public PreKeyRecord loadPreKey(Account account, int preKeyId) {
PreKeyRecord record = null;
Cursor cursor = getCursorForPreKey(account, preKeyId);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e ) {
throw new AssertionError(e);
}
}
cursor.close();
return record;
}
public boolean containsPreKey(Account account, int preKeyId) {
Cursor cursor = getCursorForPreKey(account, preKeyId);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storePreKey(Account account, PreKeyRecord record) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
}
public void deletePreKey(Account account, int preKeyId) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), Integer.toString(preKeyId)};
db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?",
args);
}
private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + AxolotlService.SQLiteAxolotlStore.ID + "=?",
selectionArgs,
null, null, null);
return cursor;
}
public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) {
SignedPreKeyRecord record = null;
Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e ) {
throw new AssertionError(e);
}
}
cursor.close();
return record;
}
public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
List<SignedPreKeyRecord> prekeys = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid()};
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
columns,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=?",
selectionArgs,
null, null, null);
while(cursor.moveToNext()) {
try {
prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)), Base64.DEFAULT)));
} catch (IOException ignored) {
}
}
cursor.close();
return prekeys;
}
public boolean containsSignedPreKey(Account account, int signedPreKeyId) {
Cursor cursor = getCursorForPreKey(account, signedPreKeyId);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId());
values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
}
public void deleteSignedPreKey(Account account, int signedPreKeyId) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ AxolotlService.SQLiteAxolotlStore.ID + "=?",
args);
}
private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
return getIdentityKeyCursor(account, name, own, null);
}
private Cursor getIdentityKeyCursor(Account account, String fingerprint) {
return getIdentityKeyCursor(account, null, null, fingerprint);
}
private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) {
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {AxolotlService.SQLiteAxolotlStore.TRUSTED,
AxolotlService.SQLiteAxolotlStore.KEY};
ArrayList<String> selectionArgs = new ArrayList<>(4);
selectionArgs.add(account.getUuid());
String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?";
if (name != null){
selectionArgs.add(name);
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.NAME + " = ?";
}
if (fingerprint != null){
selectionArgs.add(fingerprint);
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ?";
}
if (own != null){
selectionArgs.add(own?"1":"0");
selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.OWN + " = ?";
}
Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME,
columns,
selectionString,
selectionArgs.toArray(new String[selectionArgs.size()]),
null, null, null);
return cursor;
}
public IdentityKeyPair loadOwnIdentityKeyPair(Account account, String name) {
IdentityKeyPair identityKeyPair = null;
Cursor cursor = getIdentityKeyCursor(account, name, true);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
}
}
cursor.close();
return identityKeyPair;
}
public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
return loadIdentityKeys(account, name, null);
}
public Set<IdentityKey> loadIdentityKeys(Account account, String name, AxolotlService.SQLiteAxolotlStore.Trust trust) {
Set<IdentityKey> identityKeys = new HashSet<>();
Cursor cursor = getIdentityKeyCursor(account, name, false);
while(cursor.moveToNext()) {
if ( trust != null &&
cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED))
!= trust.ordinal()) {
continue;
}
try {
identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0));
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name);
}
}
cursor.close();
return identityKeys;
}
public long numTrustedKeys(Account account, String name) {
SQLiteDatabase db = getReadableDatabase();
String[] args = {
account.getUuid(),
name
};
return DatabaseUtils.queryNumEntries(db, AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?"
+ " AND " + AxolotlService.SQLiteAxolotlStore.NAME + " = ?",
args
);
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
storeIdentityKey(account, name, own, fingerprint, base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust.UNDECIDED);
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust trusted) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid());
values.put(AxolotlService.SQLiteAxolotlStore.NAME, name);
values.put(AxolotlService.SQLiteAxolotlStore.OWN, own ? 1 : 0);
values.put(AxolotlService.SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized);
values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted.ordinal());
db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
}
public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
Cursor cursor = getIdentityKeyCursor(account, fingerprint);
AxolotlService.SQLiteAxolotlStore.Trust trust = null;
if (cursor.getCount() > 0) {
cursor.moveToFirst();
int trustValue = cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED));
trust = AxolotlService.SQLiteAxolotlStore.Trust.values()[trustValue];
}
cursor.close();
return trust;
}
public boolean setIdentityKeyTrust(Account account, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) {
SQLiteDatabase db = this.getWritableDatabase();
String[] selectionArgs = {
account.getUuid(),
fingerprint
};
ContentValues values = new ContentValues();
values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal());
int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs);
return rows == 1;
}
public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
}
public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) {
storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED);
}
public void recreateAxolotlDb() {
recreateAxolotlDb(getWritableDatabase());
}
public void recreateAxolotlDb(SQLiteDatabase db) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<");
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME);
db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME);
db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
}
public void wipeAxolotlDb(Account account) {
String accountName = account.getUuid();
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<");
SQLiteDatabase db = this.getWritableDatabase();
String[] deleteArgs= {
accountName
};
db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME,
AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
}
}

View File

@ -1,5 +1,19 @@
package eu.siacs.conversations.persistance;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Base64;
import android.util.Base64OutputStream;
import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
@ -17,25 +31,11 @@ import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Base64;
import android.util.Base64OutputStream;
import android.util.Log;
import android.webkit.MimeTypeMap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;

View File

@ -1,10 +1,11 @@
package eu.siacs.conversations.services;
import eu.siacs.conversations.persistance.DatabaseBackend;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import eu.siacs.conversations.persistance.DatabaseBackend;
public class EventReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {

View File

@ -57,11 +57,11 @@ import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
@ -85,6 +85,7 @@ import eu.siacs.conversations.xmpp.OnContactStatusChanged;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
import eu.siacs.conversations.xmpp.OnNewKeysAvailable;
import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
import eu.siacs.conversations.xmpp.OnStatusChanged;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
@ -273,7 +274,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
syncDirtyContacts(account);
scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
account.getAxolotlService().publishOwnDeviceIdIfNeeded();
account.getAxolotlService().publishBundlesIfNeeded();
scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
} else if (account.getStatus() == Account.State.OFFLINE) {
resetSendingToWaiting(account);
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
@ -304,6 +308,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private int rosterChangedListenerCount = 0;
private OnMucRosterUpdate mOnMucRosterUpdate = null;
private int mucRosterChangedListenerCount = 0;
private OnNewKeysAvailable mOnNewKeysAvailable = null;
private int newKeysAvailableListenerCount = 0;
private SecureRandom mRandom;
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
@ -591,9 +597,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
for (final Account account : this.accounts) {
account.initAccountServices(this);
}
restoreFromDatabase();
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
@ -699,7 +702,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
message.getConversation().endOtrIfNeeded();
message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
markMessage(message,Message.STATUS_SEND_FAILED);
@ -753,6 +757,22 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
break;
case Message.ENCRYPTION_AXOLOTL:
if (message.needsUploading()) {
if (account.httpUploadAvailable() || message.fixCounterpart()) {
this.sendFileMessage(message);
} else {
break;
}
} else {
packet = account.getAxolotlService().fetchPacketFromCache(message);
if (packet == null) {
account.getAxolotlService().prepareMessage(message);
message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
}
}
break;
}
if (packet != null) {
if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
@ -780,6 +800,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
break;
}
}
@ -943,6 +966,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
Log.d(Config.LOGTAG,"restoring roster");
for(Account account : accounts) {
databaseBackend.readRoster(account.getRoster());
account.initAccountServices(XmppConnectionService.this);
}
getBitmapCache().evictAll();
Looper.prepare();
@ -1342,6 +1366,30 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
public void setOnNewKeysAvailableListener(final OnNewKeysAvailable listener) {
synchronized (this) {
if (checkListeners()) {
switchToForeground();
}
this.mOnNewKeysAvailable = listener;
if (this.newKeysAvailableListenerCount < 2) {
this.newKeysAvailableListenerCount++;
}
}
}
public void removeOnNewKeysAvailableListener() {
synchronized (this) {
this.newKeysAvailableListenerCount--;
if (this.newKeysAvailableListenerCount <= 0) {
this.newKeysAvailableListenerCount = 0;
this.mOnNewKeysAvailable = null;
if (checkListeners()) {
switchToBackground();
}
}
}
}
public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
synchronized (this) {
if (checkListeners()) {
@ -1372,7 +1420,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
&& this.mOnConversationUpdate == null
&& this.mOnRosterUpdate == null
&& this.mOnUpdateBlocklist == null
&& this.mOnShowErrorToast == null);
&& this.mOnShowErrorToast == null
&& this.mOnNewKeysAvailable == null);
}
private void switchToForeground() {
@ -1784,7 +1833,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
account.getJid().toBareJid() + " otr session established with "
+ conversation.getJid() + "/"
+ otrSession.getSessionID().getUserID());
conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
@ -2260,6 +2309,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
public void newKeysAvailable() {
if(mOnNewKeysAvailable != null) {
mOnNewKeysAvailable.onNewKeysAvailable();
}
}
public Account findAccountByJid(final Jid accountJid) {
for (Account account : this.accounts) {
if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {

View File

@ -2,7 +2,6 @@ package eu.siacs.conversations.ui;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.preference.Preference;
import android.util.AttributeSet;

View File

@ -4,7 +4,6 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import eu.siacs.conversations.R;

View File

@ -13,11 +13,11 @@ import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.ListView;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;

View File

@ -27,7 +27,6 @@ import org.openintents.openpgp.util.OpenPgpUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
@ -38,8 +37,8 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed {

View File

@ -29,6 +29,7 @@ import android.widget.QuickContactBadge;
import android.widget.TextView;
import org.openintents.openpgp.util.OpenPgpUtils;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.List;
@ -362,13 +363,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
ImageButton remove = (ImageButton) view
ImageButton removeButton = (ImageButton) view
.findViewById(R.id.button_remove);
remove.setVisibility(View.VISIBLE);
removeButton.setVisibility(View.VISIBLE);
keyType.setText("OTR Fingerprint");
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
keys.addView(view);
remove.setOnClickListener(new OnClickListener() {
removeButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
@ -376,6 +377,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
});
}
for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
contact.getAccount(), contact.getJid().toBareJid().toString())) {
hasKeys = true;
addFingerprintRow(keys, contact.getAccount(), identityKey);
}
if (contact.getPgpKeyId() != 0) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);

View File

@ -16,6 +16,7 @@ import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.widget.SlidingPaneLayout;
import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -28,13 +29,15 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.Toast;
import net.java.otr4j.session.SessionStatus;
import de.timroes.android.listview.EnhancedListView;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import de.timroes.android.listview.EnhancedListView;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Contact;
@ -62,11 +65,14 @@ public class ConversationActivity extends XmppActivity
public static final int REQUEST_SEND_MESSAGE = 0x0201;
public static final int REQUEST_DECRYPT_PGP = 0x0202;
public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
private static final String STATE_PANEL_OPEN = "state_panel_open";
private static final String STATE_PENDING_URI = "state_pending_uri";
@ -76,6 +82,7 @@ public class ConversationActivity extends XmppActivity
final private List<Uri> mPendingImageUris = new ArrayList<>();
final private List<Uri> mPendingFileUris = new ArrayList<>();
private Uri mPendingGeoUri = null;
private boolean forbidProcessingPendings = false;
private View mContentView;
@ -398,7 +405,7 @@ public class ConversationActivity extends XmppActivity
return true;
}
private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
final Conversation conversation = getSelectedConversation();
final Account account = conversation.getAccount();
final OnPresenceSelected callback = new OnPresenceSelected() {
@ -534,7 +541,9 @@ public class ConversationActivity extends XmppActivity
showInstallPgpDialog();
}
} else {
selectPresenceToAttachFile(attachmentChoice,encryption);
if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
selectPresenceToAttachFile(attachmentChoice, encryption);
}
}
}
@ -749,6 +758,12 @@ public class ConversationActivity extends XmppActivity
showInstallPgpDialog();
}
break;
case R.id.encryption_choice_axolotl:
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
+ "Enabled axolotl for Contact " + conversation.getContact().getJid());
conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
item.setChecked(true);
break;
default:
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
break;
@ -763,13 +778,18 @@ public class ConversationActivity extends XmppActivity
MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none);
MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp);
MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl);
if (conversation.getMode() == Conversation.MODE_MULTI) {
otr.setEnabled(false);
axolotl.setEnabled(false);
} else {
if (forceEncryption()) {
none.setVisible(false);
}
}
if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
axolotl.setEnabled(false);
}
switch (conversation.getNextEncryption(forceEncryption())) {
case Message.ENCRYPTION_NONE:
none.setChecked(true);
@ -780,6 +800,10 @@ public class ConversationActivity extends XmppActivity
case Message.ENCRYPTION_PGP:
pgp.setChecked(true);
break;
case Message.ENCRYPTION_AXOLOTL:
popup.getMenu().findItem(R.id.encryption_choice_axolotl)
.setChecked(true);
break;
default:
none.setChecked(true);
break;
@ -944,18 +968,23 @@ public class ConversationActivity extends XmppActivity
this.mConversationFragment.reInit(getSelectedConversation());
}
for(Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
attachImageToConversation(getSelectedConversation(),i.next());
if(!forbidProcessingPendings) {
for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
Uri foo = i.next();
attachImageToConversation(getSelectedConversation(), foo);
}
for(Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
attachFileToConversation(getSelectedConversation(),i.next());
for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
attachFileToConversation(getSelectedConversation(), i.next());
}
if (mPendingGeoUri != null) {
attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
mPendingGeoUri = null;
}
}
forbidProcessingPendings = false;
ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
setIntent(new Intent());
}
@ -1065,6 +1094,9 @@ public class ConversationActivity extends XmppActivity
attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
this.mPendingGeoUri = null;
}
} else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) {
this.forbidProcessingPendings = !xmppConnectionServiceBound;
mConversationFragment.onActivityResult(requestCode, resultCode, data);
}
} else {
mPendingImageUris.clear();
@ -1217,6 +1249,26 @@ public class ConversationActivity extends XmppActivity
return getPreferences().getBoolean("indicate_received", false);
}
protected boolean trustKeysIfNeeded(int requestCode) {
return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
}
protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
if(!axolotlService.getPendingKeys(mSelectedConversation.getContact()).isEmpty()
|| !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty()) {
axolotlService.createSessionsIfNeeded(mSelectedConversation, false);
Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString());
intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString());
intent.putExtra("choice", attachmentChoice);
startActivityForResult(intent, requestCode);
return true;
} else {
return false;
}
}
@Override
protected void refreshUiReal() {
updateConversationList();

View File

@ -1,5 +1,6 @@
package eu.siacs.conversations.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.PendingIntent;
@ -46,12 +47,12 @@ import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
@ -303,6 +304,10 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
sendOtrMessage(message);
} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
sendPgpMessage(message);
} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_AXOLOTL) {
if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
sendAxolotlMessage(message);
}
} else {
sendPlainTextMessage(message);
}
@ -323,6 +328,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
case Message.ENCRYPTION_OTR:
mEditMessage.setHint(getString(R.string.send_otr_message));
break;
case Message.ENCRYPTION_AXOLOTL:
mEditMessage.setHint(getString(R.string.send_axolotl_message));
break;
case Message.ENCRYPTION_PGP:
mEditMessage.setHint(getString(R.string.send_pgp_message));
break;
@ -1120,6 +1128,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
builder.create().show();
}
protected void sendAxolotlMessage(final Message message) {
final ConversationActivity activity = (ConversationActivity) getActivity();
final XmppConnectionService xmppService = activity.xmppConnectionService;
xmppService.sendMessage(message);
messageSent();
}
protected void sendOtrMessage(final Message message) {
final ConversationActivity activity = (ConversationActivity) getActivity();
final XmppConnectionService xmppService = activity.xmppConnectionService;
@ -1182,4 +1197,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
updateSendButton();
}
@Override
public void onActivityResult(int requestCode, int resultCode,
final Intent data) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
final String body = mEditMessage.getText().toString();
Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption()));
sendAxolotlMessage(message);
} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption(activity.forceEncryption()));
}
}
}
}

View File

@ -1,6 +1,8 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog.Builder;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
@ -23,6 +25,10 @@ import android.widget.TableLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
@ -54,9 +60,15 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private TextView mServerInfoPep;
private TextView mSessionEst;
private TextView mOtrFingerprint;
private TextView mAxolotlFingerprint;
private ImageView mAvatar;
private RelativeLayout mOtrFingerprintBox;
private RelativeLayout mAxolotlFingerprintBox;
private ImageButton mOtrFingerprintToClipboardButton;
private ImageButton mAxolotlFingerprintToClipboardButton;
private ImageButton mRegenerateAxolotlKeyButton;
private LinearLayout keys;
private LinearLayout keysCard;
private Jid jidToEdit;
private Account mAccount;
@ -310,6 +322,12 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box);
this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint);
this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box);
this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard);
this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key);
this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card);
this.keys = (LinearLayout) findViewById(R.id.other_device_keys);
this.mSaveButton = (Button) findViewById(R.id.save_button);
this.mCancelButton = (Button) findViewById(R.id.cancel_button);
this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener);
@ -338,6 +356,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
if (mAccount != null && mAccount.isOnlineAndConnected()) {
if (!mAccount.getXmppConnection().getFeatures().blocking()) {
showBlocklist.setVisible(false);
@ -345,6 +364,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
if (!mAccount.getXmppConnection().getFeatures().register()) {
changePassword.setVisible(false);
}
Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
if (otherDevices == null || otherDevices.isEmpty()) {
clearDevices.setVisible(false);
}
} else {
showQrCode.setVisible(false);
showBlocklist.setVisible(false);
@ -415,6 +438,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
changePasswordIntent.putExtra("account", mAccount.getJid().toString());
startActivity(changePasswordIntent);
break;
case R.id.action_clear_devices:
showWipePepDialog();
break;
}
return super.onOptionsItemSelected(item);
}
@ -477,10 +503,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} else {
this.mServerInfoPep.setText(R.string.server_info_unavailable);
}
final String fingerprint = this.mAccount.getOtrFingerprint();
if (fingerprint != null) {
final String otrFingerprint = this.mAccount.getOtrFingerprint();
if (otrFingerprint != null) {
this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint));
this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
this.mOtrFingerprintToClipboardButton
.setVisibility(View.VISIBLE);
this.mOtrFingerprintToClipboardButton
@ -489,7 +515,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View v) {
if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) {
if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_otr_fingerprint,
@ -500,6 +526,55 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} else {
this.mOtrFingerprintBox.setVisibility(View.GONE);
}
final String axolotlFingerprint = this.mAccount.getAxolotlService().getOwnPublicKey().getFingerprint();
if (axolotlFingerprint != null) {
this.mAxolotlFingerprintBox.setVisibility(View.VISIBLE);
this.mAxolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(axolotlFingerprint));
this.mAxolotlFingerprintToClipboardButton
.setVisibility(View.VISIBLE);
this.mAxolotlFingerprintToClipboardButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (copyTextToClipboard(axolotlFingerprint, R.string.axolotl_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_axolotl_fingerprint,
Toast.LENGTH_SHORT).show();
}
}
});
this.mRegenerateAxolotlKeyButton
.setVisibility(View.VISIBLE);
this.mRegenerateAxolotlKeyButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
showRegenerateAxolotlKeyDialog();
}
});
} else {
this.mAxolotlFingerprintBox.setVisibility(View.GONE);
}
final IdentityKey ownKey = mAccount.getAxolotlService().getOwnPublicKey();
boolean hasKeys = false;
keys.removeAllViews();
for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
mAccount, mAccount.getJid().toBareJid().toString())) {
if(ownKey.equals(identityKey)) {
continue;
}
hasKeys = true;
addFingerprintRow(keys, mAccount, identityKey);
}
if (hasKeys) {
keysCard.setVisibility(View.VISIBLE);
} else {
keysCard.setVisibility(View.GONE);
}
} else {
if (this.mAccount.errorStatus()) {
this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId()));
@ -512,4 +587,36 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mStats.setVisibility(View.GONE);
}
}
public void showRegenerateAxolotlKeyDialog() {
Builder builder = new Builder(this);
builder.setTitle("Regenerate Key");
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)");
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton("Yes",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAccount.getAxolotlService().regenerateKeys();
}
});
builder.create().show();
}
public void showWipePepDialog() {
Builder builder = new Builder(this);
builder.setTitle(getString(R.string.clear_other_devices));
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage(getString(R.string.clear_other_devices_desc));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.accept),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAccount.getAxolotlService().wipeOtherPepDevices();
}
});
builder.create().show();
}
}

View File

@ -1,5 +1,20 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.List;
@ -7,20 +22,6 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.AccountAdapter;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate {

View File

@ -1,19 +1,6 @@
package eu.siacs.conversations.ui;
import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xmpp.XmppConnection;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.DialogInterface;
import android.content.SharedPreferences;
@ -25,6 +12,17 @@ import android.preference.Preference;
import android.preference.PreferenceManager;
import android.widget.Toast;
import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xmpp.XmppConnection;
public class SettingsActivity extends XmppActivity implements
OnSharedPreferenceChangeListener {
private SettingsFragment mSettingsFragment;

View File

@ -17,7 +17,6 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;

View File

@ -0,0 +1,255 @@
package eu.siacs.conversations.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService.SQLiteAxolotlStore.Trust;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.xmpp.OnNewKeysAvailable;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class TrustKeysActivity extends XmppActivity implements OnNewKeysAvailable {
private Jid accountJid;
private Jid contactJid;
private boolean hasOtherTrustedKeys = false;
private boolean hasPendingFetches = false;
private Contact contact;
private TextView ownKeysTitle;
private LinearLayout ownKeys;
private LinearLayout ownKeysCard;
private TextView foreignKeysTitle;
private LinearLayout foreignKeys;
private LinearLayout foreignKeysCard;
private Button mSaveButton;
private Button mCancelButton;
private final Map<IdentityKey, Boolean> ownKeysToTrust = new HashMap<>();
private final Map<IdentityKey, Boolean> foreignKeysToTrust = new HashMap<>();
private final OnClickListener mSaveButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
commitTrusts();
Intent data = new Intent();
data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID));
setResult(RESULT_OK, data);
finish();
}
};
private final OnClickListener mCancelButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
setResult(RESULT_CANCELED);
finish();
}
};
@Override
protected void refreshUiReal() {
invalidateOptionsMenu();
populateView();
}
@Override
protected String getShareableUri() {
if (contact != null) {
return contact.getShareableUri();
} else {
return "";
}
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_trust_keys);
try {
this.accountJid = Jid.fromString(getIntent().getExtras().getString("account"));
} catch (final InvalidJidException ignored) {
}
try {
this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact"));
} catch (final InvalidJidException ignored) {
}
ownKeysTitle = (TextView) findViewById(R.id.own_keys_title);
ownKeys = (LinearLayout) findViewById(R.id.own_keys_details);
ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card);
foreignKeysTitle = (TextView) findViewById(R.id.foreign_keys_title);
foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys_details);
foreignKeysCard = (LinearLayout) findViewById(R.id.foreign_keys_card);
mCancelButton = (Button) findViewById(R.id.cancel_button);
mCancelButton.setOnClickListener(mCancelButtonListener);
mSaveButton = (Button) findViewById(R.id.save_button);
mSaveButton.setOnClickListener(mSaveButtonListener);
if (getActionBar() != null) {
getActionBar().setHomeButtonEnabled(true);
getActionBar().setDisplayHomeAsUpEnabled(true);
}
}
private void populateView() {
setTitle(getString(R.string.trust_keys));
ownKeys.removeAllViews();
foreignKeys.removeAllViews();
boolean hasOwnKeys = false;
boolean hasForeignKeys = false;
for(final IdentityKey identityKey : ownKeysToTrust.keySet()) {
hasOwnKeys = true;
addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey,
Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ownKeysToTrust.put(identityKey, isChecked);
refreshUi();
xmppConnectionService.updateAccountUi();
xmppConnectionService.updateConversationUi();
}
},
null
);
}
for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) {
hasForeignKeys = true;
addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey,
Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
foreignKeysToTrust.put(identityKey, isChecked);
refreshUi();
xmppConnectionService.updateAccountUi();
xmppConnectionService.updateConversationUi();
}
},
null
);
}
if(hasOwnKeys) {
ownKeysTitle.setText(accountJid.toString());
ownKeysCard.setVisibility(View.VISIBLE);
}
if(hasForeignKeys) {
foreignKeysTitle.setText(contactJid.toString());
foreignKeysCard.setVisibility(View.VISIBLE);
}
if(hasPendingFetches) {
setFetching();
lock();
} else {
if (!hasOtherTrustedKeys && !foreignKeysToTrust.values().contains(true)){
lock();
} else {
unlock();
}
setDone();
}
}
private void getFingerprints(final Account account) {
Set<IdentityKey> ownKeysSet = account.getAxolotlService().getPendingKeys();
for(final IdentityKey identityKey : ownKeysSet) {
if(!ownKeysToTrust.containsKey(identityKey)) {
ownKeysToTrust.put(identityKey, false);
}
}
Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getPendingKeys(contact);
for(final IdentityKey identityKey : foreignKeysSet) {
if(!foreignKeysToTrust.containsKey(identityKey)) {
foreignKeysToTrust.put(identityKey, false);
}
}
}
@Override
public void onBackendConnected() {
if ((accountJid != null) && (contactJid != null)) {
final Account account = xmppConnectionService
.findAccountByJid(accountJid);
if (account == null) {
return;
}
this.contact = account.getRoster().getContact(contactJid);
ownKeysToTrust.clear();
foreignKeysToTrust.clear();
getFingerprints(account);
if(account.getAxolotlService().getNumTrustedKeys(contact) > 0) {
hasOtherTrustedKeys = true;
}
Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, false);
if(account.getAxolotlService().hasPendingKeyFetches(conversation)) {
hasPendingFetches = true;
}
populateView();
}
}
@Override
public void onNewKeysAvailable() {
runOnUiThread(new Runnable() {
@Override
public void run() {
final Account account = xmppConnectionService
.findAccountByJid(accountJid);
hasPendingFetches = false;
getFingerprints(account);
refreshUi();
}
});
}
private void commitTrusts() {
for(IdentityKey identityKey:ownKeysToTrust.keySet()) {
contact.getAccount().getAxolotlService().setFingerprintTrust(
identityKey.getFingerprint().replaceAll("\\s", ""),
Trust.fromBoolean(ownKeysToTrust.get(identityKey)));
}
for(IdentityKey identityKey:foreignKeysToTrust.keySet()) {
contact.getAccount().getAxolotlService().setFingerprintTrust(
identityKey.getFingerprint().replaceAll("\\s", ""),
Trust.fromBoolean(foreignKeysToTrust.get(identityKey)));
}
}
private void unlock() {
mSaveButton.setEnabled(true);
mSaveButton.setTextColor(getPrimaryTextColor());
}
private void lock() {
mSaveButton.setEnabled(false);
mSaveButton.setTextColor(getSecondaryTextColor());
}
private void setDone() {
mSaveButton.setText(getString(R.string.done));
}
private void setFetching() {
mSaveButton.setText(getString(R.string.fetching_keys));
}
}

View File

@ -43,8 +43,11 @@ import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
@ -56,6 +59,8 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.java.otr4j.session.SessionID;
import org.whispersystems.libaxolotl.IdentityKey;
import java.io.FileNotFoundException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@ -65,6 +70,7 @@ import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -74,7 +80,10 @@ import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.ui.widget.Switch;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.xmpp.OnNewKeysAvailable;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@ -287,6 +296,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
}
if (this instanceof OnNewKeysAvailable) {
this.xmppConnectionService.setOnNewKeysAvailableListener((OnNewKeysAvailable) this);
}
}
protected void unregisterListeners() {
@ -308,6 +320,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.removeOnShowErrorToastListener();
}
if (this instanceof OnNewKeysAvailable) {
this.xmppConnectionService.removeOnNewKeysAvailableListener();
}
}
@Override
@ -588,6 +603,76 @@ public abstract class XmppActivity extends Activity {
builder.create().show();
}
protected void addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey) {
final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
final AxolotlService.SQLiteAxolotlStore.Trust trust = account.getAxolotlService()
.getFingerprintTrust(fingerprint);
addFingerprintRowWithListeners(keys, account, identityKey, trust, true,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked != (trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED)) {
account.getAxolotlService().setFingerprintTrust(fingerprint,
(isChecked) ? AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED :
AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED);
}
refreshUi();
xmppConnectionService.updateAccountUi();
xmppConnectionService.updateConversationUi();
}
},
new View.OnClickListener() {
@Override
public void onClick(View v) {
account.getAxolotlService().setFingerprintTrust(fingerprint,
AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED);
refreshUi();
xmppConnectionService.updateAccountUi();
xmppConnectionService.updateConversationUi();
}
}
);
}
protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account,
IdentityKey identityKey,
AxolotlService.SQLiteAxolotlStore.Trust trust,
boolean showTag,
CompoundButton.OnCheckedChangeListener
onCheckedChangeListener,
View.OnClickListener onClickListener) {
View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
trustToggle.setVisibility(View.VISIBLE);
trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
trustToggle.setOnClickListener(onClickListener);
switch (trust) {
case UNTRUSTED:
case TRUSTED:
trustToggle.setChecked(trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED, false);
trustToggle.setEnabled(true);
break;
case UNDECIDED:
trustToggle.setChecked(false, false);
trustToggle.setEnabled(false);
break;
}
if (showTag) {
keyType.setText(getString(R.string.axolotl_fingerprint));
} else {
keyType.setVisibility(View.GONE);
}
key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint()));
keys.addView(view);
}
public void selectPresence(final Conversation conversation,
final OnPresenceSelected listener) {
final Contact contact = conversation.getContact();

View File

@ -1,13 +1,5 @@
package eu.siacs.conversations.ui.adapter;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.ManageAccountActivity;
import eu.siacs.conversations.ui.widget.Switch;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
@ -17,6 +9,14 @@ import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.ManageAccountActivity;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.widget.Switch;
public class AccountAdapter extends ArrayAdapter<Account> {
private XmppActivity activity;

View File

@ -21,8 +21,8 @@ import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;

View File

@ -1,13 +1,13 @@
package eu.siacs.conversations.ui.adapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class KnownHostsAdapter extends ArrayAdapter<String> {
private ArrayList<String> domains;
private Filter domainFilter = new Filter() {

View File

@ -1,15 +1,5 @@
package eu.siacs.conversations.ui.adapter;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
@ -26,6 +16,16 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ListItemAdapter extends ArrayAdapter<ListItem> {
protected XmppActivity activity;

View File

@ -3,6 +3,7 @@ package eu.siacs.conversations.ui.adapter;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.text.Spannable;
@ -26,13 +27,14 @@ import android.widget.Toast;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper;
@ -154,6 +156,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.indicator.setVisibility(View.GONE);
} else {
viewHolder.indicator.setVisibility(View.VISIBLE);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation()
.getAccount().getAxolotlService().getFingerprintTrust(
message.getAxolotlFingerprint());
if(trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) {
viewHolder.indicator.setColorFilter(Color.RED);
} else {
viewHolder.indicator.clearColorFilter();
}
}
}
String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),

View File

@ -7,8 +7,6 @@ import android.view.ViewConfiguration;
import com.kyleduo.switchbutton.SwitchButton;
import eu.siacs.conversations.Config;
public class Switch extends SwitchButton {
private int mTouchSlop;

View File

@ -96,11 +96,10 @@ public final class CryptoHelper {
} else if (fingerprint.length() < 40) {
return fingerprint;
}
StringBuilder builder = new StringBuilder(fingerprint);
builder.insert(8, " ");
builder.insert(17, " ");
builder.insert(26, " ");
builder.insert(35, " ");
StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s",""));
for(int i=8;i<builder.length();i+=9) {
builder.insert(i, ' ');
}
return builder.toString();
}

View File

@ -1,17 +1,7 @@
package eu.siacs.conversations.utils;
import de.measite.minidns.Client;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.Record;
import de.measite.minidns.Record.TYPE;
import de.measite.minidns.Record.CLASS;
import de.measite.minidns.record.SRV;
import de.measite.minidns.record.A;
import de.measite.minidns.record.AAAA;
import de.measite.minidns.record.Data;
import de.measite.minidns.util.NameUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
@ -22,8 +12,18 @@ import java.util.Random;
import java.util.TreeMap;
import java.util.regex.Pattern;
import android.os.Bundle;
import android.util.Log;
import de.measite.minidns.Client;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.Record;
import de.measite.minidns.Record.CLASS;
import de.measite.minidns.Record.TYPE;
import de.measite.minidns.record.A;
import de.measite.minidns.record.AAAA;
import de.measite.minidns.record.Data;
import de.measite.minidns.record.SRV;
import de.measite.minidns.util.NameUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.jid.Jid;
public class DNSHelper {

View File

@ -1,5 +1,7 @@
package eu.siacs.conversations.utils;
import android.content.Context;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
@ -8,8 +10,6 @@ import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import android.content.Context;
public class ExceptionHandler implements UncaughtExceptionHandler {
private UncaughtExceptionHandler defaultHandler;

View File

@ -1,5 +1,17 @@
package eu.siacs.conversations.utils;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
import android.text.format.DateUtils;
import android.util.Log;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
@ -15,18 +27,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.DialogInterface.OnClickListener;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
import android.text.format.DateUtils;
import android.util.Log;
public class ExceptionHelper {
public static void init(Context context) {
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {

View File

@ -1,9 +1,9 @@
package eu.siacs.conversations.utils;
import java.util.List;
import android.os.Bundle;
import java.util.List;
public interface OnPhoneContactsLoadedListener {
public void onPhoneContactsLoaded(List<Bundle> phoneContacts);
}

View File

@ -1,9 +1,5 @@
package eu.siacs.conversations.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Loader;
@ -15,6 +11,9 @@ import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Profile;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
public class PhoneHelper {
public static void loadPhoneContacts(Context context,final List<Bundle> phoneContacts, final OnPhoneContactsLoadedListener listener) {

View File

@ -1,5 +1,10 @@
package eu.siacs.conversations.utils;
import android.content.Context;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@ -9,15 +14,10 @@ import java.util.Locale;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.content.Context;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Pair;
public class UIHelper {
private static String BLACK_HEART_SUIT = "\u2665";

View File

@ -21,6 +21,11 @@ public class Element {
this.name = name;
}
public Element(String name, String xmlns) {
this.name = name;
this.setAttribute("xmlns", xmlns);
}
public Element addChild(Element child) {
this.content = null;
children.add(child);

View File

@ -1,19 +1,19 @@
package eu.siacs.conversations.xml;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import eu.siacs.conversations.Config;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import eu.siacs.conversations.Config;
public class XmlReader {
private XmlPullParser parser;
private PowerManager.WakeLock wakeLock;

View File

@ -0,0 +1,5 @@
package eu.siacs.conversations.xmpp;
public interface OnNewKeysAvailable {
public void onNewKeysAvailable();
}

View File

@ -26,7 +26,6 @@ import java.net.IDN;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;

View File

@ -1,5 +1,10 @@
package eu.siacs.conversations.xmpp.jingle;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@ -8,17 +13,13 @@ import java.util.Locale;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;

View File

@ -1,16 +1,18 @@
package eu.siacs.conversations.xmpp.jingle;
import android.annotation.SuppressLint;
import android.util.Log;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import android.annotation.SuppressLint;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Xmlns;

View File

@ -1,5 +1,8 @@
package eu.siacs.conversations.xmpp.jingle;
import android.util.Base64;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -7,9 +10,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import android.util.Base64;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;

View File

@ -1,10 +1,10 @@
package eu.siacs.conversations.xmpp.pep;
import android.util.Base64;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.util.Base64;
public class Avatar {
public enum Origin { PEP, VCARD };

View File

@ -2,8 +2,6 @@ package eu.siacs.conversations.xmpp.stanzas;
import android.util.Pair;
import java.text.ParseException;
import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.xml.Element;
@ -29,6 +27,11 @@ public class MessagePacket extends AbstractStanza {
this.children.add(0, body);
}
public void setAxolotlMessage(Element axolotlMessage) {
this.children.remove(findChild("body"));
this.children.add(0, axolotlMessage);
}
public void setType(int type) {
switch (type) {
case TYPE_CHAT:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -342,6 +342,95 @@
android:visibility="visible"
android:contentDescription="@string/copy_otr_clipboard_description"/>
</RelativeLayout>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/axolotl_fingerprint_box"
android:layout_marginTop="32dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/axolotl_actions"
android:orientation="vertical">
<TextView
android:id="@+id/axolotl_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black87"
android:textSize="?attr/TextSizeBody"
android:typeface="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black54"
android:textSize="?attr/TextSizeInfo"
android:text="@string/this_device_axolotl_fingerprint"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/axolotl_actions"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:orientation="vertical">
<ImageButton
android:id="@+id/action_copy_axolotl_to_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_copy"
android:visibility="visible"
android:contentDescription="@string/copy_axolotl_clipboard_description"/>
<ImageButton
android:id="@+id/action_regenerate_axolotl_key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_refresh"
android:visibility="visible"
android:contentDescription="@string/regenerate_axolotl_key"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/other_device_keys_card"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:background="@drawable/infocard_border"
android:orientation="vertical"
android:padding="@dimen/infocard_padding"
android:visibility="gone">
<TextView
android:id="@+id/other_device_keys_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black87"
android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold"
android:text="@string/other_devices"/>
<LinearLayout
android:id="@+id/other_device_keys"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:divider="?android:dividerHorizontal"
android:orientation="vertical"
android:showDividers="middle" >
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/grey200" >
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/button_bar"
android:layout_alignParentTop="true" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/own_keys_card"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:background="@drawable/infocard_border"
android:orientation="vertical"
android:padding="@dimen/infocard_padding"
android:visibility="gone">
<TextView
android:id="@+id/own_keys_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black87"
android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold"/>
<LinearLayout
android:id="@+id/own_keys_details"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:divider="?android:dividerHorizontal"
android:showDividers="middle"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/foreign_keys_card"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:background="@drawable/infocard_border"
android:orientation="vertical"
android:padding="@dimen/infocard_padding"
android:visibility="gone">
<TextView
android:id="@+id/foreign_keys_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black87"
android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold"/>
<LinearLayout
android:id="@+id/foreign_keys_details"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:divider="?android:dividerHorizontal"
android:showDividers="middle"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/button_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true" >
<Button
android:id="@+id/cancel_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/cancel"
android:textColor="@color/black87" />
<View
android:layout_width="1dp"
android:layout_height="fill_parent"
android:layout_marginBottom="7dp"
android:layout_marginTop="7dp"
android:background="@color/black12" />
<Button
android:id="@+id/save_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:enabled="true"
android:textColor="@color/black54"
android:text="@string/done"/>
</LinearLayout>
</RelativeLayout>

View File

@ -3,12 +3,11 @@
android:layout_width="wrap_content"
android:layout_height="match_parent" >
<LinearLayout
<RelativeLayout
android:id="@+id/key_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/button_remove"
android:orientation="vertical"
android:padding="8dp" >
<TextView
@ -16,6 +15,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black87"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/tgl_trust"
android:textSize="?attr/TextSizeBody"
android:typeface="monospace" />
@ -24,18 +25,41 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black54"
android:layout_alignParentLeft="true"
android:layout_below="@+id/key"
android:textSize="?attr/TextSizeInfo"/>
<TextView
android:id="@+id/key_trust"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_below="@+id/key"
android:visibility="gone"
android:textColor="@color/black54"
android:textSize="?attr/TextSizeInfo"/>
</LinearLayout>
<ImageButton
android:id="@+id/button_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_toRightOf="@+id/key"
android:layout_centerVertical="true"
android:background="?android:selectableItemBackground"
android:padding="@dimen/image_button_padding"
android:src="?attr/icon_remove"
android:visibility="invisible" />
android:visibility="gone" />
<eu.siacs.conversations.ui.widget.Switch
android:id="@+id/tgl_trust"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
style="@style/MD"/>
</RelativeLayout>
</RelativeLayout>

View File

@ -20,4 +20,8 @@
<item android:id="@+id/action_change_password_on_server"
android:title="@string/change_password"
android:showAsAction="never" />
<item android:id="@+id/action_clear_devices"
android:title="@string/clear_other_devices"
android:showAsAction="never"/>
</menu>

View File

@ -11,6 +11,9 @@
<item
android:id="@+id/encryption_choice_pgp"
android:title="@string/encryption_choice_pgp"/>
<item
android:id="@+id/encryption_choice_axolotl"
android:title="@string/encryption_choice_axolotl"/>
</group>
</menu>

View File

@ -21,8 +21,10 @@
<item name="attr/icon_download">@drawable/ic_file_download_white_24dp</item>
<item name="attr/icon_edit">@drawable/ic_edit_white_24dp</item>
<item name="attr/icon_edit_dark">@drawable/ic_edit_grey600_24dp</item>
<item name="attr/icon_done">@drawable/ic_done_black_24dp</item>
<item name="attr/icon_group">@drawable/ic_group_white_24dp</item>
<item name="attr/icon_new">@drawable/ic_add_white_24dp</item>
<item name="attr/icon_refresh">@drawable/ic_refresh_grey600_24dp</item>
<item name="attr/icon_new_attachment">@drawable/ic_attach_file_white_24dp</item>
<item name="attr/icon_not_secure">@drawable/ic_lock_open_white_24dp</item>
<item name="attr/icon_remove">@drawable/ic_delete_grey600_24dp</item>

View File

@ -14,6 +14,7 @@
<attr name="icon_download" format="reference"/>
<attr name="icon_edit" format="reference"/>
<attr name="icon_edit_dark" format="reference"/>
<attr name="icon_done" format="reference"/>
<attr name="icon_group" format="reference"/>
<attr name="icon_new" format="reference"/>
<attr name="icon_new_attachment" format="reference"/>

View File

@ -80,6 +80,7 @@
<string name="choose_presence">Choose presence to contact</string>
<string name="send_plain_text_message">Send plain text message</string>
<string name="send_otr_message">Send OTR encrypted message</string>
<string name="send_axolotl_message">Send Axolotl encrypted message</string>
<string name="send_pgp_message">Send OpenPGP encrypted message</string>
<string name="your_nick_has_been_changed">Your nickname has been changed</string>
<string name="send_unencrypted">Send unencrypted</string>
@ -154,6 +155,7 @@
<string name="encryption_choice_none">Plain text</string>
<string name="encryption_choice_otr">OTR</string>
<string name="encryption_choice_pgp">OpenPGP</string>
<string name="encryption_choice_axolotl">Axolotl</string>
<string name="mgmt_account_edit">Edit account</string>
<string name="mgmt_account_delete">Delete account</string>
<string name="mgmt_account_disable">Temporarily disable</string>
@ -206,6 +208,13 @@
<string name="reception_failed">Reception failed</string>
<string name="your_fingerprint">Your fingerprint</string>
<string name="otr_fingerprint">OTR fingerprint</string>
<string name="axolotl_fingerprint">Axolotl fingerprint</string>
<string name="this_device_axolotl_fingerprint">Own Axolotl fingerprint</string>
<string name="other_devices">Other devices</string>
<string name="trust_keys">Trust Axolotl Keys</string>
<string name="fetching_keys">Fetching keys...</string>
<string name="done">Done</string>
<string name="axolotl_devicelist">Other own Axolotl Devices</string>
<string name="verify">Verify</string>
<string name="decrypt">Decrypt</string>
<string name="conferences">Conferences</string>
@ -312,6 +321,7 @@
<string name="pref_conference_name">Conference name</string>
<string name="pref_conference_name_summary">Use rooms subject instead of JID to identify conferences</string>
<string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string>
<string name="toast_message_axolotl_fingerprint">Axolotl fingerprint copied to clipboard!</string>
<string name="conference_banned">You are banned from this conference</string>
<string name="conference_members_only">This conference is members only</string>
<string name="conference_kicked">You have been kicked from this conference</string>
@ -378,6 +388,11 @@
<string name="reset">Reset</string>
<string name="account_image_description">Account avatar</string>
<string name="copy_otr_clipboard_description">Copy OTR fingerprint to clipboard</string>
<string name="copy_axolotl_clipboard_description">Copy Axolotl fingerprint to clipboard</string>
<string name="regenerate_axolotl_key">Copy Axolotl fingerprint to clipboard</string>
<string name="wipe_axolotl_pep">Wipe other devices from PEP</string>
<string name="clear_other_devices">Clear devices</string>
<string name="clear_other_devices_desc">Are you sure you want to clear all other devices from the axolotl announcement? The next time your devices connect, they will reannounce themselves, but they might not receive messages sent in the meantime.</string>
<string name="fetching_history_from_server">Fetching history from server</string>
<string name="no_more_history_on_server">No more history on server</string>
<string name="updating">Updating…</string>

View File

@ -18,6 +18,7 @@
<item name="attr/icon_download">@drawable/ic_action_download</item>
<item name="attr/icon_edit">@drawable/ic_action_edit</item>
<item name="attr/icon_edit_dark">@drawable/ic_action_edit_dark</item>
<item name="attr/icon_done">@drawable/ic_action_done</item>
<item name="attr/icon_group">@drawable/ic_action_group</item>
<item name="attr/icon_new">@drawable/ic_action_new</item>