Compare commits
49 commits
master
...
CryptoNext
Author | SHA1 | Date | |
---|---|---|---|
|
3b279673ef | ||
|
cbfd07d580 | ||
|
235c9039e0 | ||
|
625cab74cc | ||
|
91b1225124 | ||
|
8b08bdb3af | ||
|
36e7431595 | ||
|
7ebfe7f108 | ||
|
3615b12cbb | ||
|
cc661dc477 | ||
|
f0afcc4b52 | ||
|
26c8365a5d | ||
|
2afb36d891 | ||
|
bdc9f9a44f | ||
|
b1e719bd8b | ||
|
ce4b86e6d4 | ||
|
34f90f2eb7 | ||
|
2f487c7947 | ||
|
121919def1 | ||
|
6b0d286518 | ||
|
9e3419722b | ||
|
4f6ca6fb63 | ||
|
d5b3557157 | ||
|
9d780a382a | ||
|
7cdf2a9946 | ||
|
30403a70f2 | ||
|
4b0279a6ef | ||
|
3b8dfafecd | ||
|
835584ae3b | ||
|
4cc4e81b8e | ||
|
72619de889 | ||
|
12fc24dd42 | ||
|
c5596b34bc | ||
|
9206a49b79 | ||
|
7680a24180 | ||
|
c1116b6066 | ||
|
ab2f85d2e8 | ||
|
a58d5e8ce3 | ||
|
ae75c571df | ||
|
ba9520729f | ||
|
287ce131d8 | ||
|
046a2d6045 | ||
|
71c0a75ec9 | ||
|
0423852cb8 | ||
|
e8e126f2ce | ||
|
61f18d4dfc | ||
|
6805abbef0 | ||
|
0917a75705 | ||
|
f16b77d382 |
|
@ -36,6 +36,7 @@ dependencies {
|
|||
compile 'de.measite.minidns:minidns:0.1.3'
|
||||
compile 'de.timroes.android:EnhancedListView:0.3.4'
|
||||
compile 'me.leolin:ShortcutBadger:1.1.1@aar'
|
||||
compile 'org.whispersystems:axolotl-android:1.3.4'
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
public class NoSessionsCreatedException extends Throwable{
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.InvalidKeyException;
|
||||
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.entities.Contact;
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -2,15 +2,20 @@ package eu.siacs.conversations.entities;
|
|||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
|
@ -183,20 +188,22 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(ACCOUNT, accountUuid);
|
||||
values.put(SYSTEMNAME, systemName);
|
||||
values.put(SERVERNAME, serverName);
|
||||
values.put(JID, jid.toString());
|
||||
values.put(OPTIONS, subscription);
|
||||
values.put(SYSTEMACCOUNT, systemAccount);
|
||||
values.put(PHOTOURI, photoUri);
|
||||
values.put(KEYS, keys.toString());
|
||||
values.put(AVATAR, avatar == null ? null : avatar.getFilename());
|
||||
values.put(LAST_PRESENCE, lastseen.presence);
|
||||
values.put(LAST_TIME, lastseen.time);
|
||||
values.put(GROUPS, groups.toString());
|
||||
return values;
|
||||
synchronized (this.keys) {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(ACCOUNT, accountUuid);
|
||||
values.put(SYSTEMNAME, systemName);
|
||||
values.put(SERVERNAME, serverName);
|
||||
values.put(JID, jid.toString());
|
||||
values.put(OPTIONS, subscription);
|
||||
values.put(SYSTEMACCOUNT, systemAccount);
|
||||
values.put(PHOTOURI, photoUri);
|
||||
values.put(KEYS, keys.toString());
|
||||
values.put(AVATAR, avatar == null ? null : avatar.getFilename());
|
||||
values.put(LAST_PRESENCE, lastseen.presence);
|
||||
values.put(LAST_TIME, lastseen.time);
|
||||
values.put(GROUPS, groups.toString());
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
public int getSubscription() {
|
||||
|
@ -281,60 +288,65 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
|
||||
public ArrayList<String> getOtrFingerprints() {
|
||||
final ArrayList<String> fingerprints = new ArrayList<String>();
|
||||
try {
|
||||
if (this.keys.has("otr_fingerprints")) {
|
||||
final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
|
||||
for (int i = 0; i < prints.length(); ++i) {
|
||||
final String print = prints.isNull(i) ? null : prints.getString(i);
|
||||
if (print != null && !print.isEmpty()) {
|
||||
fingerprints.add(prints.getString(i));
|
||||
synchronized (this.keys) {
|
||||
final ArrayList<String> fingerprints = new ArrayList<String>();
|
||||
try {
|
||||
if (this.keys.has("otr_fingerprints")) {
|
||||
final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
|
||||
for (int i = 0; i < prints.length(); ++i) {
|
||||
final String print = prints.isNull(i) ? null : prints.getString(i);
|
||||
if (print != null && !print.isEmpty()) {
|
||||
fingerprints.add(prints.getString(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (final JSONException ignored) {
|
||||
} catch (final JSONException ignored) {
|
||||
|
||||
}
|
||||
return fingerprints;
|
||||
}
|
||||
return fingerprints;
|
||||
}
|
||||
|
||||
public boolean addOtrFingerprint(String print) {
|
||||
if (getOtrFingerprints().contains(print)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JSONArray fingerprints;
|
||||
if (!this.keys.has("otr_fingerprints")) {
|
||||
fingerprints = new JSONArray();
|
||||
|
||||
} else {
|
||||
fingerprints = this.keys.getJSONArray("otr_fingerprints");
|
||||
synchronized (this.keys) {
|
||||
if (getOtrFingerprints().contains(print)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JSONArray fingerprints;
|
||||
if (!this.keys.has("otr_fingerprints")) {
|
||||
fingerprints = new JSONArray();
|
||||
} else {
|
||||
fingerprints = this.keys.getJSONArray("otr_fingerprints");
|
||||
}
|
||||
fingerprints.put(print);
|
||||
this.keys.put("otr_fingerprints", fingerprints);
|
||||
return true;
|
||||
} catch (final JSONException ignored) {
|
||||
return false;
|
||||
}
|
||||
fingerprints.put(print);
|
||||
this.keys.put("otr_fingerprints", fingerprints);
|
||||
return true;
|
||||
} catch (final JSONException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public long getPgpKeyId() {
|
||||
if (this.keys.has("pgp_keyid")) {
|
||||
try {
|
||||
return this.keys.getLong("pgp_keyid");
|
||||
} catch (JSONException e) {
|
||||
synchronized (this.keys) {
|
||||
if (this.keys.has("pgp_keyid")) {
|
||||
try {
|
||||
return this.keys.getLong("pgp_keyid");
|
||||
} catch (JSONException e) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void setPgpKeyId(long keyId) {
|
||||
try {
|
||||
this.keys.put("pgp_keyid", keyId);
|
||||
} catch (final JSONException ignored) {
|
||||
|
||||
synchronized (this.keys) {
|
||||
try {
|
||||
this.keys.put("pgp_keyid", keyId);
|
||||
} catch (final JSONException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -441,24 +453,26 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
|
||||
public boolean deleteOtrFingerprint(String fingerprint) {
|
||||
boolean success = false;
|
||||
try {
|
||||
if (this.keys.has("otr_fingerprints")) {
|
||||
JSONArray newPrints = new JSONArray();
|
||||
JSONArray oldPrints = this.keys
|
||||
.getJSONArray("otr_fingerprints");
|
||||
for (int i = 0; i < oldPrints.length(); ++i) {
|
||||
if (!oldPrints.getString(i).equals(fingerprint)) {
|
||||
newPrints.put(oldPrints.getString(i));
|
||||
} else {
|
||||
success = true;
|
||||
synchronized (this.keys) {
|
||||
boolean success = false;
|
||||
try {
|
||||
if (this.keys.has("otr_fingerprints")) {
|
||||
JSONArray newPrints = new JSONArray();
|
||||
JSONArray oldPrints = this.keys
|
||||
.getJSONArray("otr_fingerprints");
|
||||
for (int i = 0; i < oldPrints.length(); ++i) {
|
||||
if (!oldPrints.getString(i).equals(fingerprint)) {
|
||||
newPrints.put(oldPrints.getString(i));
|
||||
} else {
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
this.keys.put("otr_fingerprints", newPrints);
|
||||
}
|
||||
this.keys.put("otr_fingerprints", newPrints);
|
||||
return success;
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
}
|
||||
return success;
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -179,13 +179,13 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 Downloadable downloadable = 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;
|
||||
}
|
||||
|
||||
|
@ -381,7 +389,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()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -653,4 +662,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
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;
|
||||
|
@ -115,6 +124,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");
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.siacs.conversations.generator;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
@ -7,6 +9,11 @@ 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.NoSessionsCreatedException;
|
||||
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 +66,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);
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -6,7 +6,12 @@ import android.util.Pair;
|
|||
import net.java.otr4j.session.Session;
|
||||
import net.java.otr4j.session.SessionStatus;
|
||||
|
||||
import java.util.List;
|
||||
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 +99,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 +189,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 +258,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 +288,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 +321,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);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,31 @@
|
|||
package eu.siacs.conversations.persistance;
|
||||
|
||||
import android.content.ContentValues;
|
||||
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.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 +34,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, "
|
||||
|
@ -33,12 +47,66 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ Contact.JID + " TEXT," + Contact.KEYS + " TEXT,"
|
||||
+ Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER,"
|
||||
+ Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, "
|
||||
+ Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
|
||||
+ Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
|
||||
+ Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
|
||||
+ Account.TABLENAME + "(" + Account.UUID
|
||||
+ ") 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 +137,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
|
||||
|
@ -109,12 +182,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN "
|
||||
+ Conversation.ATTRIBUTES + " TEXT");
|
||||
}
|
||||
if (oldVersion < 9 && newVersion >= 9) {
|
||||
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
|
||||
+ Contact.LAST_TIME + " NUMBER");
|
||||
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
|
||||
+ Contact.LAST_PRESENCE + " TEXT");
|
||||
}
|
||||
if (oldVersion < 9 && newVersion >= 9) {
|
||||
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
|
||||
+ Contact.LAST_TIME + " NUMBER");
|
||||
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
|
||||
+ Contact.LAST_PRESENCE + " TEXT");
|
||||
}
|
||||
if (oldVersion < 10 && newVersion >= 10) {
|
||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
|
||||
+ Message.RELATIVE_FILE_PATH + " TEXT");
|
||||
|
@ -215,6 +288,11 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
cursor.close();
|
||||
}
|
||||
if (oldVersion < 15 && newVersion >= 15) {
|
||||
recreateAxolotlDb();
|
||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
|
||||
+ Message.FINGERPRINT + " TEXT");
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized DatabaseBackend getInstance(Context context) {
|
||||
|
@ -311,7 +389,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 +559,378 @@ 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) {
|
||||
Set<IdentityKey> identityKeys = new HashSet<>();
|
||||
Cursor cursor = getIdentityKeyCursor(account, name, false);
|
||||
|
||||
while(cursor.moveToNext()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<");
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -272,7 +272,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)) {
|
||||
|
@ -590,9 +593,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);
|
||||
|
@ -698,7 +698,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);
|
||||
|
@ -752,6 +753,15 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
|
|||
}
|
||||
}
|
||||
break;
|
||||
case Message.ENCRYPTION_AXOLOTL:
|
||||
message.setStatus(Message.STATUS_WAITING);
|
||||
packet = account.getAxolotlService().fetchPacketFromCache(message);
|
||||
if (packet == null && account.isOnlineAndConnected()) {
|
||||
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) {
|
||||
|
@ -779,6 +789,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 +956,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();
|
||||
|
@ -1757,7 +1771,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) {
|
||||
|
|
|
@ -29,11 +29,13 @@ import android.widget.QuickContactBadge;
|
|||
import android.widget.TextView;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.PgpEngine;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.ListItem;
|
||||
|
@ -362,13 +364,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 +378,57 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
}
|
||||
});
|
||||
}
|
||||
for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
|
||||
contact.getAccount(), contact.getJid().toBareJid().toString())) {
|
||||
hasKeys = true;
|
||||
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);
|
||||
TextView keyTrust = (TextView) view.findViewById(R.id.key_trust);
|
||||
ImageButton removeButton = (ImageButton) view
|
||||
.findViewById(R.id.button_remove);
|
||||
ImageButton trustButton = (ImageButton) view
|
||||
.findViewById(R.id.button_trust);
|
||||
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
|
||||
final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
|
||||
final Jid bareJid = contactJid.toBareJid();
|
||||
AxolotlService.SQLiteAxolotlStore.Trust trust = contact.getAccount().getAxolotlService()
|
||||
.getFingerprintTrust(fingerprint);
|
||||
switch (trust) {
|
||||
case TRUSTED:
|
||||
removeButton.setVisibility(View.VISIBLE);
|
||||
//Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting remove button visible!");
|
||||
break;
|
||||
case UNDECIDED:
|
||||
case UNTRUSTED:
|
||||
//Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting trust button visible!");
|
||||
trustButton.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
keyType.setText("Axolotl Fingerprint");
|
||||
key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint()));
|
||||
keyTrust.setText(trust.toString());
|
||||
keyTrust.setVisibility(View.VISIBLE);
|
||||
keys.addView(view);
|
||||
removeButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
axolotlService.setFingerprintTrust(fingerprint,
|
||||
AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED);
|
||||
refreshUi();
|
||||
xmppConnectionService.updateConversationUi();
|
||||
}
|
||||
});
|
||||
trustButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
axolotlService.setFingerprintTrust(fingerprint,
|
||||
AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED);
|
||||
refreshUi();
|
||||
xmppConnectionService.updateConversationUi();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (contact.getPgpKeyId() != 0) {
|
||||
hasKeys = true;
|
||||
View view = inflater.inflate(R.layout.contact_key, keys, false);
|
||||
|
|
|
@ -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;
|
||||
|
@ -34,7 +35,9 @@ 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.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Blockable;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
|
@ -752,6 +755,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;
|
||||
|
@ -764,15 +773,20 @@ public class ConversationActivity extends XmppActivity
|
|||
});
|
||||
popup.inflate(R.menu.encryption_choices);
|
||||
MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
|
||||
MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl);
|
||||
MenuItem none = popup.getMenu().findItem(
|
||||
R.id.encryption_choice_none);
|
||||
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);
|
||||
|
@ -784,6 +798,10 @@ public class ConversationActivity extends XmppActivity
|
|||
popup.getMenu().findItem(R.id.encryption_choice_pgp)
|
||||
.setChecked(true);
|
||||
break;
|
||||
case Message.ENCRYPTION_AXOLOTL:
|
||||
popup.getMenu().findItem(R.id.encryption_choice_axolotl)
|
||||
.setChecked(true);
|
||||
break;
|
||||
default:
|
||||
popup.getMenu().findItem(R.id.encryption_choice_none)
|
||||
.setChecked(true);
|
||||
|
|
|
@ -302,6 +302,8 @@ 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) {
|
||||
sendAxolotlMessage(message);
|
||||
} else {
|
||||
sendPlainTextMessage(message);
|
||||
}
|
||||
|
@ -322,6 +324,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;
|
||||
|
@ -1114,6 +1119,14 @@ 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;
|
||||
//message.setCounterpart(conversation.getNextCounterpart());
|
||||
xmppService.sendMessage(message);
|
||||
messageSent();
|
||||
}
|
||||
|
||||
protected void sendOtrMessage(final Message message) {
|
||||
final ConversationActivity activity = (ConversationActivity) getActivity();
|
||||
final XmppConnectionService xmppService = activity.xmppConnectionService;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
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;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -23,6 +26,8 @@ import android.widget.TableLayout;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
|
||||
|
@ -54,9 +59,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
|
|||
private TextView mServerInfoPep;
|
||||
private TextView mSessionEst;
|
||||
private TextView mOtrFingerprint;
|
||||
private TextView mAxolotlFingerprint;
|
||||
private TextView mAxolotlDevicelist;
|
||||
private ImageView mAvatar;
|
||||
private RelativeLayout mOtrFingerprintBox;
|
||||
private RelativeLayout mAxolotlFingerprintBox;
|
||||
private RelativeLayout mAxolotlDevicelistBox;
|
||||
private ImageButton mOtrFingerprintToClipboardButton;
|
||||
private ImageButton mAxolotlFingerprintToClipboardButton;
|
||||
private ImageButton mWipeAxolotlPepButton;
|
||||
private ImageButton mRegenerateAxolotlKeyButton;
|
||||
|
||||
private Jid jidToEdit;
|
||||
private Account mAccount;
|
||||
|
@ -310,6 +322,13 @@ 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.mAxolotlDevicelist = (TextView) findViewById(R.id.axolotl_devicelist);
|
||||
this.mAxolotlDevicelistBox = (RelativeLayout) findViewById(R.id.axolotl_devices_box);
|
||||
this.mWipeAxolotlPepButton = (ImageButton) findViewById(R.id.action_wipe_axolotl_pep);
|
||||
this.mSaveButton = (Button) findViewById(R.id.save_button);
|
||||
this.mCancelButton = (Button) findViewById(R.id.cancel_button);
|
||||
this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener);
|
||||
|
@ -475,10 +494,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
|
||||
|
@ -487,7 +506,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,
|
||||
|
@ -498,6 +517,55 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
|
|||
} else {
|
||||
this.mOtrFingerprintBox.setVisibility(View.GONE);
|
||||
}
|
||||
final Set<Integer> ownDevices = this.mAccount.getAxolotlService().getOwnDeviceIds();
|
||||
if (ownDevices != null && !ownDevices.isEmpty()) {
|
||||
this.mAxolotlDevicelistBox.setVisibility(View.VISIBLE);
|
||||
this.mAxolotlDevicelist.setText(TextUtils.join(", ", ownDevices));
|
||||
this.mWipeAxolotlPepButton
|
||||
.setVisibility(View.VISIBLE);
|
||||
this.mWipeAxolotlPepButton
|
||||
.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
showWipePepDialog();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.mAxolotlDevicelistBox.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);
|
||||
}
|
||||
} else {
|
||||
if (this.mAccount.errorStatus()) {
|
||||
this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId()));
|
||||
|
@ -508,4 +576,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("Wipe PEP");
|
||||
builder.setIconAttribute(android.R.attr.alertDialogIcon);
|
||||
builder.setMessage("Are you sure you want to wipe all other devices from the PEP device ID list?");
|
||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||
builder.setPositiveButton("Yes",
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mAccount.getAxolotlService().wipeOtherPepDevices();
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,6 +27,7 @@ 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;
|
||||
|
@ -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(),
|
||||
|
|
|
@ -94,11 +94,10 @@ public final class CryptoHelper {
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -29,6 +29,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:
|
||||
|
|
BIN
src/main/res/drawable-hdpi/ic_action_done.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/main/res/drawable-hdpi/ic_done_black_24dp.png
Normal file
After Width: | Height: | Size: 177 B |
BIN
src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png
Normal file
After Width: | Height: | Size: 508 B |
BIN
src/main/res/drawable-mdpi/ic_action_done.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/main/res/drawable-mdpi/ic_done_black_24dp.png
Normal file
After Width: | Height: | Size: 130 B |
BIN
src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png
Normal file
After Width: | Height: | Size: 356 B |
BIN
src/main/res/drawable-xhdpi/ic_action_done.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/main/res/drawable-xhdpi/ic_done_black_24dp.png
Normal file
After Width: | Height: | Size: 188 B |
BIN
src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png
Normal file
After Width: | Height: | Size: 644 B |
BIN
src/main/res/drawable-xxhdpi/ic_done_black_24dp.png
Normal file
After Width: | Height: | Size: 227 B |
BIN
src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png
Normal file
After Width: | Height: | Size: 882 B |
BIN
src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png
Normal file
After Width: | Height: | Size: 277 B |
BIN
src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
|
@ -342,6 +342,107 @@
|
|||
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/primarytext"
|
||||
android:textSize="?attr/TextSizeBody"
|
||||
android:typeface="monospace" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/secondarytext"
|
||||
android:textSize="?attr/TextSizeInfo"
|
||||
android:text="@string/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>
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/axolotl_devices_box"
|
||||
android:layout_marginTop="32dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_toLeftOf="@+id/action_wipe_axolotl_pep"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/axolotl_devicelist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/primarytext"
|
||||
android:textSize="?attr/TextSizeBody"
|
||||
android:typeface="monospace" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/secondarytext"
|
||||
android:textSize="?attr/TextSizeInfo"
|
||||
android:text="@string/axolotl_devicelist"/>
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/action_wipe_axolotl_pep"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:padding="@dimen/image_button_padding"
|
||||
android:src="?attr/icon_remove"
|
||||
android:visibility="visible"
|
||||
android:contentDescription="@string/wipe_axolotl_pep"/>
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
|
|
@ -3,18 +3,18 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<LinearLayout
|
||||
<RelativeLayout
|
||||
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
|
||||
android:id="@+id/key"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:textColor="@color/primarytext"
|
||||
android:textSize="?attr/TextSizeBody"
|
||||
android:typeface="monospace" />
|
||||
|
@ -23,9 +23,21 @@
|
|||
android:id="@+id/key_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_below="@+id/key"
|
||||
android:textColor="@color/secondarytext"
|
||||
android:textSize="?attr/TextSizeInfo"/>
|
||||
</LinearLayout>
|
||||
|
||||
<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/secondarytext"
|
||||
android:textSize="?attr/TextSizeInfo"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_remove"
|
||||
|
@ -38,4 +50,14 @@
|
|||
android:src="?attr/icon_remove"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_trust"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:padding="@dimen/image_button_padding"
|
||||
android:src="?attr/icon_done"
|
||||
android:visibility="invisible" />
|
||||
</RelativeLayout>
|
|
@ -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>
|
|
@ -18,8 +18,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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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,8 @@
|
|||
<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="axolotl_devicelist">Other own Axolotl Devices</string>
|
||||
<string name="verify">Verify</string>
|
||||
<string name="decrypt">Decrypt</string>
|
||||
<string name="conferences">Conferences</string>
|
||||
|
@ -312,6 +316,7 @@
|
|||
<string name="pref_conference_name">Conference name</string>
|
||||
<string name="pref_conference_name_summary">Use room’s 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>
|
||||
|
@ -379,6 +384,9 @@
|
|||
<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="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>
|
||||
|
|
|
@ -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>
|
||||
|
|